Validator blob signing for the unblinded flow (#4096)

* Implement validator blob signing (full block and full blob)

* Fix compilation error and remove redundant slot check

* Fix clippy error
This commit is contained in:
Jimmy Chen 2023-03-18 00:29:25 +11:00 committed by GitHub
parent 3c18e1a3a4
commit 1301c62436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 218 additions and 114 deletions

View File

@ -1326,7 +1326,7 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> Into<BeaconBlock<T, Payload>>
pub type BlockContentsTuple<T, Payload> = ( pub type BlockContentsTuple<T, Payload> = (
SignedBeaconBlock<T, Payload>, SignedBeaconBlock<T, Payload>,
Option<VariableList<SignedBlobSidecar<T>, <T as EthSpec>::MaxBlobsPerBlock>>, Option<SignedBlobSidecarList<T>>,
); );
/// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBeaconBlockAndBlobSidecars`]. /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBeaconBlockAndBlobSidecars`].
@ -1374,9 +1374,25 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> From<SignedBeaconBlock<T, Payl
} }
} }
impl<T: EthSpec, Payload: AbstractExecPayload<T>> From<BlockContentsTuple<T, Payload>>
for SignedBlockContents<T, Payload>
{
fn from(block_contents_tuple: BlockContentsTuple<T, Payload>) -> Self {
match block_contents_tuple {
(signed_block, None) => SignedBlockContents::Block(signed_block),
(signed_block, Some(signed_blob_sidecars)) => {
SignedBlockContents::BlockAndBlobSidecars(SignedBeaconBlockAndBlobSidecars {
signed_block,
signed_blob_sidecars,
})
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Encode)] #[derive(Debug, Clone, Serialize, Deserialize, Encode)]
#[serde(bound = "T: EthSpec")] #[serde(bound = "T: EthSpec")]
pub struct SignedBeaconBlockAndBlobSidecars<T: EthSpec, Payload: AbstractExecPayload<T>> { pub struct SignedBeaconBlockAndBlobSidecars<T: EthSpec, Payload: AbstractExecPayload<T>> {
pub signed_block: SignedBeaconBlock<T, Payload>, pub signed_block: SignedBeaconBlock<T, Payload>,
pub signed_blob_sidecars: VariableList<SignedBlobSidecar<T>, <T as EthSpec>::MaxBlobsPerBlock>, pub signed_blob_sidecars: SignedBlobSidecarList<T>,
} }

View File

@ -14,7 +14,7 @@ pub enum Domain {
BlsToExecutionChange, BlsToExecutionChange,
BeaconProposer, BeaconProposer,
BeaconAttester, BeaconAttester,
BlobsSideCar, BlobSidecar,
Randao, Randao,
Deposit, Deposit,
VoluntaryExit, VoluntaryExit,
@ -100,7 +100,7 @@ pub struct ChainSpec {
*/ */
pub(crate) domain_beacon_proposer: u32, pub(crate) domain_beacon_proposer: u32,
pub(crate) domain_beacon_attester: u32, pub(crate) domain_beacon_attester: u32,
pub(crate) domain_blobs_sidecar: u32, pub(crate) domain_blob_sidecar: u32,
pub(crate) domain_randao: u32, pub(crate) domain_randao: u32,
pub(crate) domain_deposit: u32, pub(crate) domain_deposit: u32,
pub(crate) domain_voluntary_exit: u32, pub(crate) domain_voluntary_exit: u32,
@ -366,7 +366,7 @@ impl ChainSpec {
match domain { match domain {
Domain::BeaconProposer => self.domain_beacon_proposer, Domain::BeaconProposer => self.domain_beacon_proposer,
Domain::BeaconAttester => self.domain_beacon_attester, Domain::BeaconAttester => self.domain_beacon_attester,
Domain::BlobsSideCar => self.domain_blobs_sidecar, Domain::BlobSidecar => self.domain_blob_sidecar,
Domain::Randao => self.domain_randao, Domain::Randao => self.domain_randao,
Domain::Deposit => self.domain_deposit, Domain::Deposit => self.domain_deposit,
Domain::VoluntaryExit => self.domain_voluntary_exit, Domain::VoluntaryExit => self.domain_voluntary_exit,
@ -574,7 +574,7 @@ impl ChainSpec {
domain_voluntary_exit: 4, domain_voluntary_exit: 4,
domain_selection_proof: 5, domain_selection_proof: 5,
domain_aggregate_and_proof: 6, domain_aggregate_and_proof: 6,
domain_blobs_sidecar: 10, // 0x0a000000 domain_blob_sidecar: 11, // 0x0B000000
/* /*
* Fork choice * Fork choice
@ -809,7 +809,7 @@ impl ChainSpec {
domain_voluntary_exit: 4, domain_voluntary_exit: 4,
domain_selection_proof: 5, domain_selection_proof: 5,
domain_aggregate_and_proof: 6, domain_aggregate_and_proof: 6,
domain_blobs_sidecar: 10, domain_blob_sidecar: 11,
/* /*
* Fork choice * Fork choice
@ -1285,7 +1285,7 @@ mod tests {
test_domain(Domain::BeaconProposer, spec.domain_beacon_proposer, &spec); test_domain(Domain::BeaconProposer, spec.domain_beacon_proposer, &spec);
test_domain(Domain::BeaconAttester, spec.domain_beacon_attester, &spec); test_domain(Domain::BeaconAttester, spec.domain_beacon_attester, &spec);
test_domain(Domain::BlobsSideCar, spec.domain_blobs_sidecar, &spec); test_domain(Domain::BlobSidecar, spec.domain_blob_sidecar, &spec);
test_domain(Domain::Randao, spec.domain_randao, &spec); test_domain(Domain::Randao, spec.domain_randao, &spec);
test_domain(Domain::Deposit, spec.domain_deposit, &spec); test_domain(Domain::Deposit, spec.domain_deposit, &spec);
test_domain(Domain::VoluntaryExit, spec.domain_voluntary_exit, &spec); test_domain(Domain::VoluntaryExit, spec.domain_voluntary_exit, &spec);
@ -1311,7 +1311,7 @@ mod tests {
&spec, &spec,
); );
test_domain(Domain::BlobsSideCar, spec.domain_blobs_sidecar, &spec); test_domain(Domain::BlobSidecar, spec.domain_blob_sidecar, &spec);
} }
fn apply_bit_mask(domain_bytes: [u8; 4], spec: &ChainSpec) -> u32 { fn apply_bit_mask(domain_bytes: [u8; 4], spec: &ChainSpec) -> u32 {

View File

@ -78,7 +78,7 @@ pub fn get_extra_fields(spec: &ChainSpec) -> HashMap<String, Value> {
"bls_withdrawal_prefix".to_uppercase() => u8_hex(spec.bls_withdrawal_prefix_byte), "bls_withdrawal_prefix".to_uppercase() => u8_hex(spec.bls_withdrawal_prefix_byte),
"domain_beacon_proposer".to_uppercase() => u32_hex(spec.domain_beacon_proposer), "domain_beacon_proposer".to_uppercase() => u32_hex(spec.domain_beacon_proposer),
"domain_beacon_attester".to_uppercase() => u32_hex(spec.domain_beacon_attester), "domain_beacon_attester".to_uppercase() => u32_hex(spec.domain_beacon_attester),
"domain_blobs_sidecar".to_uppercase() => u32_hex(spec.domain_blobs_sidecar), "domain_blob_sidecar".to_uppercase() => u32_hex(spec.domain_blob_sidecar),
"domain_randao".to_uppercase()=> u32_hex(spec.domain_randao), "domain_randao".to_uppercase()=> u32_hex(spec.domain_randao),
"domain_deposit".to_uppercase()=> u32_hex(spec.domain_deposit), "domain_deposit".to_uppercase()=> u32_hex(spec.domain_deposit),
"domain_voluntary_exit".to_uppercase() => u32_hex(spec.domain_voluntary_exit), "domain_voluntary_exit".to_uppercase() => u32_hex(spec.domain_voluntary_exit),

View File

@ -2,6 +2,7 @@ use crate::{test_utils::TestRandom, BlobSidecar, EthSpec, Signature};
use derivative::Derivative; use derivative::Derivative;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode}; use ssz_derive::{Decode, Encode};
use ssz_types::VariableList;
use test_random_derive::TestRandom; use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash; use tree_hash_derive::TreeHash;
@ -25,3 +26,6 @@ pub struct SignedBlobSidecar<T: EthSpec> {
pub message: BlobSidecar<T>, pub message: BlobSidecar<T>,
pub signature: Signature, pub signature: Signature,
} }
pub type SignedBlobSidecarList<T> =
VariableList<SignedBlobSidecar<T>, <T as EthSpec>::MaxBlobsPerBlock>;

View File

@ -6,9 +6,11 @@ use crate::{
OfflineOnFailure, OfflineOnFailure,
}; };
use crate::{http_metrics::metrics, validator_store::ValidatorStore}; use crate::{http_metrics::metrics, validator_store::ValidatorStore};
use bls::SignatureBytes;
use environment::RuntimeContext; use environment::RuntimeContext;
use eth2::types::SignedBlockContents; use eth2::types::{BlockContents, SignedBlockContents};
use slog::{crit, debug, error, info, trace, warn}; use eth2::BeaconNodeHttpClient;
use slog::{crit, debug, error, info, trace, warn, Logger};
use slot_clock::SlotClock; use slot_clock::SlotClock;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
@ -16,8 +18,8 @@ use std::time::Duration;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::time::sleep; use tokio::time::sleep;
use types::{ use types::{
AbstractExecPayload, BeaconBlock, BlindedPayload, BlockType, EthSpec, FullPayload, Graffiti, AbstractExecPayload, BlindedPayload, BlockType, EthSpec, FullPayload, Graffiti, PublicKeyBytes,
PublicKeyBytes, Slot, Slot,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -342,80 +344,46 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
"slot" => slot.as_u64(), "slot" => slot.as_u64(),
); );
// Request block from first responsive beacon node. // Request block from first responsive beacon node.
let block = self let block_contents = self
.beacon_nodes .beacon_nodes
.first_success( .first_success(
RequireSynced::No, RequireSynced::No,
OfflineOnFailure::Yes, OfflineOnFailure::Yes,
|beacon_node| async move { move |beacon_node| {
let block: BeaconBlock<E, Payload> = match Payload::block_type() { Self::get_validator_block(
BlockType::Full => { beacon_node,
let _get_timer = metrics::start_timer_vec( slot,
&metrics::BLOCK_SERVICE_TIMES, randao_reveal_ref,
&[metrics::BEACON_BLOCK_HTTP_GET], graffiti,
); proposer_index,
beacon_node
.get_validator_blocks::<E, Payload>(
slot,
randao_reveal_ref,
graffiti.as_ref(),
)
.await
.map_err(|e| {
BlockError::Recoverable(format!(
"Error from beacon node when producing block: {:?}",
e
))
})?
.data
.into()
}
BlockType::Blinded => {
let _get_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BLINDED_BEACON_BLOCK_HTTP_GET],
);
beacon_node
.get_validator_blinded_blocks::<E, Payload>(
slot,
randao_reveal_ref,
graffiti.as_ref(),
)
.await
.map_err(|e| {
BlockError::Recoverable(format!(
"Error from beacon node when producing block: {:?}",
e
))
})?
.data
}
};
info!(
log, log,
"Received unsigned block"; )
"slot" => slot.as_u64(),
);
if proposer_index != Some(block.proposer_index()) {
return Err(BlockError::Recoverable(
"Proposer index does not match block proposer. Beacon chain re-orged"
.to_string(),
));
}
Ok::<_, BlockError>(block)
}, },
) )
.await?; .await?;
let (block, maybe_blob_sidecars) = block_contents.deconstruct();
let signing_timer = metrics::start_timer(&metrics::BLOCK_SIGNING_TIMES); let signing_timer = metrics::start_timer(&metrics::BLOCK_SIGNING_TIMES);
let signed_block_contents: SignedBlockContents<E, Payload> = self_ref
let signed_block = self_ref
.validator_store .validator_store
.sign_block::<Payload>(*validator_pubkey_ref, block, current_slot) .sign_block::<Payload>(*validator_pubkey_ref, block, current_slot)
.await .await
.map_err(|e| BlockError::Recoverable(format!("Unable to sign block: {:?}", e)))? .map_err(|e| BlockError::Recoverable(format!("Unable to sign block: {:?}", e)))?;
.into();
let maybe_signed_blobs = match maybe_blob_sidecars {
Some(blob_sidecars) => Some(
self_ref
.validator_store
.sign_blobs(*validator_pubkey_ref, blob_sidecars)
.await
.map_err(|e| {
BlockError::Recoverable(format!("Unable to sign blob: {:?}", e))
})?,
),
None => None,
};
let signing_time_ms = let signing_time_ms =
Duration::from_secs_f64(signing_timer.map_or(0.0, |t| t.stop_and_record())).as_millis(); Duration::from_secs_f64(signing_timer.map_or(0.0, |t| t.stop_and_record())).as_millis();
@ -426,46 +394,19 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
"signing_time_ms" => signing_time_ms, "signing_time_ms" => signing_time_ms,
); );
let signed_block_contents = SignedBlockContents::from((signed_block, maybe_signed_blobs));
// Publish block with first available beacon node. // Publish block with first available beacon node.
self.beacon_nodes self.beacon_nodes
.first_success( .first_success(
RequireSynced::No, RequireSynced::No,
OfflineOnFailure::Yes, OfflineOnFailure::Yes,
|beacon_node| async { |beacon_node| async {
match Payload::block_type() { Self::publish_signed_block_contents::<Payload>(
BlockType::Full => { &signed_block_contents,
let _post_timer = metrics::start_timer_vec( beacon_node,
&metrics::BLOCK_SERVICE_TIMES, )
&[metrics::BEACON_BLOCK_HTTP_POST], .await
);
beacon_node
.post_beacon_blocks(&signed_block_contents)
.await
.map_err(|e| {
BlockError::Irrecoverable(format!(
"Error from beacon node when publishing block: {:?}",
e
))
})?
}
BlockType::Blinded => {
let _post_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BLINDED_BEACON_BLOCK_HTTP_POST],
);
beacon_node
// TODO: need to be adjusted for blobs
.post_beacon_blinded_blocks(signed_block_contents.signed_block())
.await
.map_err(|e| {
BlockError::Irrecoverable(format!(
"Error from beacon node when publishing block: {:?}",
e
))
})?
}
}
Ok::<_, BlockError>(())
}, },
) )
.await?; .await?;
@ -482,4 +423,106 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
Ok(()) Ok(())
} }
async fn publish_signed_block_contents<Payload: AbstractExecPayload<E>>(
signed_block_contents: &SignedBlockContents<E, Payload>,
beacon_node: &BeaconNodeHttpClient,
) -> Result<(), BlockError> {
match Payload::block_type() {
BlockType::Full => {
let _post_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BEACON_BLOCK_HTTP_POST],
);
beacon_node
.post_beacon_blocks(signed_block_contents)
.await
.map_err(|e| {
BlockError::Irrecoverable(format!(
"Error from beacon node when publishing block: {:?}",
e
))
})?
}
BlockType::Blinded => {
let _post_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BLINDED_BEACON_BLOCK_HTTP_POST],
);
todo!("need to be adjusted for blobs");
// beacon_node
// .post_beacon_blinded_blocks(signed_block_contents.signed_block())
// .await
// .map_err(|e| {
// BlockError::Irrecoverable(format!(
// "Error from beacon node when publishing block: {:?}",
// e
// ))
// })?
}
}
Ok::<_, BlockError>(())
}
async fn get_validator_block<Payload: AbstractExecPayload<E>>(
beacon_node: &BeaconNodeHttpClient,
slot: Slot,
randao_reveal_ref: &SignatureBytes,
graffiti: Option<Graffiti>,
proposer_index: Option<u64>,
log: &Logger,
) -> Result<BlockContents<E, Payload>, BlockError> {
let block_contents: BlockContents<E, Payload> = match Payload::block_type() {
BlockType::Full => {
let _get_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BEACON_BLOCK_HTTP_GET],
);
beacon_node
.get_validator_blocks::<E, Payload>(slot, randao_reveal_ref, graffiti.as_ref())
.await
.map_err(|e| {
BlockError::Recoverable(format!(
"Error from beacon node when producing block: {:?}",
e
))
})?
.data
}
BlockType::Blinded => {
let _get_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BLINDED_BEACON_BLOCK_HTTP_GET],
);
todo!("implement blinded flow for blobs");
// beacon_node
// .get_validator_blinded_blocks::<E, Payload>(
// slot,
// randao_reveal_ref,
// graffiti.as_ref(),
// )
// .await
// .map_err(|e| {
// BlockError::Recoverable(format!(
// "Error from beacon node when producing block: {:?}",
// e
// ))
// })?
// .data
}
};
info!(
log,
"Received unsigned block";
"slot" => slot.as_u64(),
);
if proposer_index != Some(block_contents.block().proposer_index()) {
return Err(BlockError::Recoverable(
"Proposer index does not match block proposer. Beacon chain re-orged".to_string(),
));
}
Ok::<_, BlockError>(block_contents)
}
} }

View File

@ -37,6 +37,7 @@ pub enum Error {
pub enum SignableMessage<'a, T: EthSpec, Payload: AbstractExecPayload<T> = FullPayload<T>> { pub enum SignableMessage<'a, T: EthSpec, Payload: AbstractExecPayload<T> = FullPayload<T>> {
RandaoReveal(Epoch), RandaoReveal(Epoch),
BeaconBlock(&'a BeaconBlock<T, Payload>), BeaconBlock(&'a BeaconBlock<T, Payload>),
BlobSidecar(&'a BlobSidecar<T>),
AttestationData(&'a AttestationData), AttestationData(&'a AttestationData),
SignedAggregateAndProof(&'a AggregateAndProof<T>), SignedAggregateAndProof(&'a AggregateAndProof<T>),
SelectionProof(Slot), SelectionProof(Slot),
@ -58,6 +59,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload<T>> SignableMessage<'a, T, Pay
match self { match self {
SignableMessage::RandaoReveal(epoch) => epoch.signing_root(domain), SignableMessage::RandaoReveal(epoch) => epoch.signing_root(domain),
SignableMessage::BeaconBlock(b) => b.signing_root(domain), SignableMessage::BeaconBlock(b) => b.signing_root(domain),
SignableMessage::BlobSidecar(b) => b.signing_root(domain),
SignableMessage::AttestationData(a) => a.signing_root(domain), SignableMessage::AttestationData(a) => a.signing_root(domain),
SignableMessage::SignedAggregateAndProof(a) => a.signing_root(domain), SignableMessage::SignedAggregateAndProof(a) => a.signing_root(domain),
SignableMessage::SelectionProof(slot) => slot.signing_root(domain), SignableMessage::SelectionProof(slot) => slot.signing_root(domain),
@ -180,6 +182,10 @@ impl SigningMethod {
Web3SignerObject::RandaoReveal { epoch } Web3SignerObject::RandaoReveal { epoch }
} }
SignableMessage::BeaconBlock(block) => Web3SignerObject::beacon_block(block)?, SignableMessage::BeaconBlock(block) => Web3SignerObject::beacon_block(block)?,
SignableMessage::BlobSidecar(_) => {
// https://github.com/ConsenSys/web3signer/issues/726
unimplemented!("Web3Signer blob signing not implemented.")
}
SignableMessage::AttestationData(a) => Web3SignerObject::Attestation(a), SignableMessage::AttestationData(a) => Web3SignerObject::Attestation(a),
SignableMessage::SignedAggregateAndProof(a) => { SignableMessage::SignedAggregateAndProof(a) => {
Web3SignerObject::AggregateAndProof(a) Web3SignerObject::AggregateAndProof(a)

View File

@ -6,6 +6,7 @@ use crate::{
Config, Config,
}; };
use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString};
use eth2::types::VariableList;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use slashing_protection::{ use slashing_protection::{
interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase,
@ -19,11 +20,12 @@ use std::sync::Arc;
use task_executor::TaskExecutor; use task_executor::TaskExecutor;
use types::{ use types::{
attestation::Error as AttestationError, graffiti::GraffitiString, AbstractExecPayload, Address, attestation::Error as AttestationError, graffiti::GraffitiString, AbstractExecPayload, Address,
AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, BlobSidecarList, ChainSpec,
Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, Keypair, PublicKeyBytes, SelectionProof, ContributionAndProof, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, Keypair, PublicKeyBytes,
Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedRoot, SelectionProof, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedBlobSidecar,
SignedValidatorRegistrationData, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, SignedBlobSidecarList, SignedContributionAndProof, SignedRoot, SignedValidatorRegistrationData,
SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage,
SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData,
}; };
use validator_dir::ValidatorDir; use validator_dir::ValidatorDir;
@ -531,6 +533,39 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
} }
} }
pub async fn sign_blobs(
&self,
validator_pubkey: PublicKeyBytes,
blob_sidecars: BlobSidecarList<E>,
) -> Result<SignedBlobSidecarList<E>, Error> {
let mut signed_blob_sidecars = Vec::new();
for blob_sidecar in blob_sidecars.into_iter() {
let slot = blob_sidecar.slot;
let signing_epoch = slot.epoch(E::slots_per_epoch());
let signing_context = self.signing_context(Domain::BlobSidecar, signing_epoch);
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
let signature = signing_method
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::BlobSidecar(&blob_sidecar),
signing_context,
&self.spec,
&self.task_executor,
)
.await?;
metrics::inc_counter_vec(&metrics::SIGNED_BLOBS_TOTAL, &[metrics::SUCCESS]);
signed_blob_sidecars.push(SignedBlobSidecar {
message: blob_sidecar,
signature,
});
}
Ok(VariableList::from(signed_blob_sidecars))
}
pub async fn sign_attestation( pub async fn sign_attestation(
&self, &self,
validator_pubkey: PublicKeyBytes, validator_pubkey: PublicKeyBytes,