Add SSZ support to validator block production endpoints (#4534)

## Issue Addressed

#4531 

## Proposed Changes

add SSZ support to the following block production endpoints:

GET /eth/v2/validator/blocks/{slot}
GET /eth/v1/validator/blinded_blocks/{slot}

## Additional Info

i updated a few existing tests to use ssz instead of writing completely new tests
This commit is contained in:
Eitan Seri-Levi 2023-09-21 06:38:31 +00:00
parent a0478da990
commit 992b476eac
4 changed files with 255 additions and 19 deletions

View File

@ -3002,6 +3002,7 @@ pub fn serve<T: BeaconChainTypes>(
.and(warp::path::end()) .and(warp::path::end())
.and(not_while_syncing_filter.clone()) .and(not_while_syncing_filter.clone())
.and(warp::query::<api_types::ValidatorBlocksQuery>()) .and(warp::query::<api_types::ValidatorBlocksQuery>())
.and(warp::header::optional::<api_types::Accept>("accept"))
.and(task_spawner_filter.clone()) .and(task_spawner_filter.clone())
.and(chain_filter.clone()) .and(chain_filter.clone())
.and(log_filter.clone()) .and(log_filter.clone())
@ -3009,6 +3010,7 @@ pub fn serve<T: BeaconChainTypes>(
|endpoint_version: EndpointVersion, |endpoint_version: EndpointVersion,
slot: Slot, slot: Slot,
query: api_types::ValidatorBlocksQuery, query: api_types::ValidatorBlocksQuery,
accept_header: Option<api_types::Accept>,
task_spawner: TaskSpawner<T::EthSpec>, task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>, chain: Arc<BeaconChain<T>>,
log: Logger| { log: Logger| {
@ -3053,9 +3055,24 @@ pub fn serve<T: BeaconChainTypes>(
.fork_name(&chain.spec) .fork_name(&chain.spec)
.map_err(inconsistent_fork_rejection)?; .map_err(inconsistent_fork_rejection)?;
fork_versioned_response(endpoint_version, fork_name, block) match accept_header {
.map(|response| warp::reply::json(&response).into_response()) Some(api_types::Accept::Ssz) => Response::builder()
.map(|res| add_consensus_version_header(res, fork_name)) .status(200)
.header("Content-Type", "application/octet-stream")
.body(block.as_ssz_bytes().into())
.map(|res: Response<Bytes>| {
add_consensus_version_header(res, fork_name)
})
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"failed to create response: {}",
e
))
}),
_ => fork_versioned_response(endpoint_version, fork_name, block)
.map(|response| warp::reply::json(&response).into_response())
.map(|res| add_consensus_version_header(res, fork_name)),
}
}) })
}, },
); );
@ -3072,11 +3089,13 @@ pub fn serve<T: BeaconChainTypes>(
.and(warp::path::end()) .and(warp::path::end())
.and(not_while_syncing_filter.clone()) .and(not_while_syncing_filter.clone())
.and(warp::query::<api_types::ValidatorBlocksQuery>()) .and(warp::query::<api_types::ValidatorBlocksQuery>())
.and(warp::header::optional::<api_types::Accept>("accept"))
.and(task_spawner_filter.clone()) .and(task_spawner_filter.clone())
.and(chain_filter.clone()) .and(chain_filter.clone())
.then( .then(
|slot: Slot, |slot: Slot,
query: api_types::ValidatorBlocksQuery, query: api_types::ValidatorBlocksQuery,
accept_header: Option<api_types::Accept>,
task_spawner: TaskSpawner<T::EthSpec>, task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>| { chain: Arc<BeaconChain<T>>| {
task_spawner.spawn_async_with_rejection(Priority::P0, async move { task_spawner.spawn_async_with_rejection(Priority::P0, async move {
@ -3114,10 +3133,25 @@ pub fn serve<T: BeaconChainTypes>(
.fork_name(&chain.spec) .fork_name(&chain.spec)
.map_err(inconsistent_fork_rejection)?; .map_err(inconsistent_fork_rejection)?;
// Pose as a V2 endpoint so we return the fork `version`. match accept_header {
fork_versioned_response(V2, fork_name, block) Some(api_types::Accept::Ssz) => Response::builder()
.map(|response| warp::reply::json(&response).into_response()) .status(200)
.map(|res| add_consensus_version_header(res, fork_name)) .header("Content-Type", "application/octet-stream")
.body(block.as_ssz_bytes().into())
.map(|res: Response<Bytes>| {
add_consensus_version_header(res, fork_name)
})
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"failed to create response: {}",
e
))
}),
// Pose as a V2 endpoint so we return the fork `version`.
_ => fork_versioned_response(V2, fork_name, block)
.map(|response| warp::reply::json(&response).into_response())
.map(|res| add_consensus_version_header(res, fork_name)),
}
}) })
}, },
); );

View File

@ -2513,6 +2513,70 @@ impl ApiTester {
self self
} }
pub async fn test_block_production_ssz(self) -> Self {
let fork = self.chain.canonical_head.cached_head().head_fork();
let genesis_validators_root = self.chain.genesis_validators_root;
for _ in 0..E::slots_per_epoch() * 3 {
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let proposer_pubkey_bytes = self
.client
.get_validator_duties_proposer(epoch)
.await
.unwrap()
.data
.into_iter()
.find(|duty| duty.slot == slot)
.map(|duty| duty.pubkey)
.unwrap();
let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap();
let sk = self
.validator_keypairs()
.iter()
.find(|kp| kp.pk == proposer_pubkey)
.map(|kp| kp.sk.clone())
.unwrap();
let randao_reveal = {
let domain = self.chain.spec.get_domain(
epoch,
Domain::Randao,
&fork,
genesis_validators_root,
);
let message = epoch.signing_root(domain);
sk.sign(message).into()
};
let block_bytes = self
.client
.get_validator_blocks_ssz::<E, FullPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.expect("block bytes");
let block =
BeaconBlock::<E, FullPayload<E>>::from_ssz_bytes(&block_bytes, &self.chain.spec)
.expect("block bytes can be decoded");
let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec);
self.client
.post_beacon_blocks_ssz(&signed_block)
.await
.unwrap();
assert_eq!(self.chain.head_beacon_block().as_ref(), &signed_block);
self.chain.slot_clock.set_slot(slot.as_u64() + 1);
}
self
}
pub async fn test_block_production_no_verify_randao(self) -> Self { pub async fn test_block_production_no_verify_randao(self) -> Self {
for _ in 0..E::slots_per_epoch() { for _ in 0..E::slots_per_epoch() {
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@ -2694,12 +2758,15 @@ impl ApiTester {
sk.sign(message).into() sk.sign(message).into()
}; };
let block = self let block_bytes = self
.client .client
.get_validator_blinded_blocks::<E, Payload>(slot, &randao_reveal, None) .get_validator_blinded_blocks_ssz::<E, Payload>(slot, &randao_reveal, None)
.await .await
.unwrap() .unwrap()
.data; .expect("block bytes");
let block = BeaconBlock::<E, Payload>::from_ssz_bytes(&block_bytes, &self.chain.spec)
.expect("block bytes can be decoded");
let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec);
@ -4891,6 +4958,20 @@ async fn block_production_verify_randao_invalid() {
.await; .await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn block_production_ssz_full_payload() {
ApiTester::new().await.test_block_production_ssz().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn block_production_ssz_with_skip_slots() {
ApiTester::new()
.await
.skip_slots(E::slots_per_epoch() * 2)
.test_block_production_ssz()
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn blinded_block_production_full_payload_premerge() { async fn blinded_block_production_full_payload_premerge() {
ApiTester::new() ApiTester::new()

View File

@ -120,6 +120,7 @@ pub struct Timeouts {
pub get_beacon_blocks_ssz: Duration, pub get_beacon_blocks_ssz: Duration,
pub get_debug_beacon_states: Duration, pub get_debug_beacon_states: Duration,
pub get_deposit_snapshot: Duration, pub get_deposit_snapshot: Duration,
pub get_validator_block_ssz: Duration,
} }
impl Timeouts { impl Timeouts {
@ -135,6 +136,7 @@ impl Timeouts {
get_beacon_blocks_ssz: timeout, get_beacon_blocks_ssz: timeout,
get_debug_beacon_states: timeout, get_debug_beacon_states: timeout,
get_deposit_snapshot: timeout, get_deposit_snapshot: timeout,
get_validator_block_ssz: timeout,
} }
} }
} }
@ -1598,14 +1600,14 @@ impl BeaconNodeHttpClient {
.await .await
} }
/// `GET v2/validator/blocks/{slot}` /// returns `GET v2/validator/blocks/{slot}` URL path
pub async fn get_validator_blocks_modular<T: EthSpec, Payload: AbstractExecPayload<T>>( pub async fn get_validator_blocks_path<T: EthSpec, Payload: AbstractExecPayload<T>>(
&self, &self,
slot: Slot, slot: Slot,
randao_reveal: &SignatureBytes, randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>, graffiti: Option<&Graffiti>,
skip_randao_verification: SkipRandaoVerification, skip_randao_verification: SkipRandaoVerification,
) -> Result<ForkVersionedResponse<BeaconBlock<T, Payload>>, Error> { ) -> Result<Url, Error> {
let mut path = self.eth_path(V2)?; let mut path = self.eth_path(V2)?;
path.path_segments_mut() path.path_segments_mut()
@ -1627,9 +1629,66 @@ impl BeaconNodeHttpClient {
.append_pair("skip_randao_verification", ""); .append_pair("skip_randao_verification", "");
} }
Ok(path)
}
/// `GET v2/validator/blocks/{slot}`
pub async fn get_validator_blocks_modular<T: EthSpec, Payload: AbstractExecPayload<T>>(
&self,
slot: Slot,
randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>,
skip_randao_verification: SkipRandaoVerification,
) -> Result<ForkVersionedResponse<BeaconBlock<T, Payload>>, Error> {
let path = self
.get_validator_blocks_path::<T, Payload>(
slot,
randao_reveal,
graffiti,
skip_randao_verification,
)
.await?;
self.get(path).await self.get(path).await
} }
/// `GET v2/validator/blocks/{slot}` in ssz format
pub async fn get_validator_blocks_ssz<T: EthSpec, Payload: AbstractExecPayload<T>>(
&self,
slot: Slot,
randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>,
) -> Result<Option<Vec<u8>>, Error> {
self.get_validator_blocks_modular_ssz::<T, Payload>(
slot,
randao_reveal,
graffiti,
SkipRandaoVerification::No,
)
.await
}
/// `GET v2/validator/blocks/{slot}` in ssz format
pub async fn get_validator_blocks_modular_ssz<T: EthSpec, Payload: AbstractExecPayload<T>>(
&self,
slot: Slot,
randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>,
skip_randao_verification: SkipRandaoVerification,
) -> Result<Option<Vec<u8>>, Error> {
let path = self
.get_validator_blocks_path::<T, Payload>(
slot,
randao_reveal,
graffiti,
skip_randao_verification,
)
.await?;
self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block_ssz)
.await
}
/// `GET v2/validator/blinded_blocks/{slot}` /// `GET v2/validator/blinded_blocks/{slot}`
pub async fn get_validator_blinded_blocks<T: EthSpec, Payload: AbstractExecPayload<T>>( pub async fn get_validator_blinded_blocks<T: EthSpec, Payload: AbstractExecPayload<T>>(
&self, &self,
@ -1646,17 +1705,14 @@ impl BeaconNodeHttpClient {
.await .await
} }
/// `GET v1/validator/blinded_blocks/{slot}` /// returns `GET v1/validator/blinded_blocks/{slot}` URL path
pub async fn get_validator_blinded_blocks_modular< pub async fn get_validator_blinded_blocks_path<T: EthSpec, Payload: AbstractExecPayload<T>>(
T: EthSpec,
Payload: AbstractExecPayload<T>,
>(
&self, &self,
slot: Slot, slot: Slot,
randao_reveal: &SignatureBytes, randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>, graffiti: Option<&Graffiti>,
skip_randao_verification: SkipRandaoVerification, skip_randao_verification: SkipRandaoVerification,
) -> Result<ForkVersionedResponse<BeaconBlock<T, Payload>>, Error> { ) -> Result<Url, Error> {
let mut path = self.eth_path(V1)?; let mut path = self.eth_path(V1)?;
path.path_segments_mut() path.path_segments_mut()
@ -1678,9 +1734,71 @@ impl BeaconNodeHttpClient {
.append_key_only("skip_randao_verification"); .append_key_only("skip_randao_verification");
} }
Ok(path)
}
/// `GET v1/validator/blinded_blocks/{slot}`
pub async fn get_validator_blinded_blocks_modular<
T: EthSpec,
Payload: AbstractExecPayload<T>,
>(
&self,
slot: Slot,
randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>,
skip_randao_verification: SkipRandaoVerification,
) -> Result<ForkVersionedResponse<BeaconBlock<T, Payload>>, Error> {
let path = self
.get_validator_blinded_blocks_path::<T, Payload>(
slot,
randao_reveal,
graffiti,
skip_randao_verification,
)
.await?;
self.get(path).await self.get(path).await
} }
/// `GET v2/validator/blinded_blocks/{slot}` in ssz format
pub async fn get_validator_blinded_blocks_ssz<T: EthSpec, Payload: AbstractExecPayload<T>>(
&self,
slot: Slot,
randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>,
) -> Result<Option<Vec<u8>>, Error> {
self.get_validator_blinded_blocks_modular_ssz::<T, Payload>(
slot,
randao_reveal,
graffiti,
SkipRandaoVerification::No,
)
.await
}
pub async fn get_validator_blinded_blocks_modular_ssz<
T: EthSpec,
Payload: AbstractExecPayload<T>,
>(
&self,
slot: Slot,
randao_reveal: &SignatureBytes,
graffiti: Option<&Graffiti>,
skip_randao_verification: SkipRandaoVerification,
) -> Result<Option<Vec<u8>>, Error> {
let path = self
.get_validator_blinded_blocks_path::<T, Payload>(
slot,
randao_reveal,
graffiti,
skip_randao_verification,
)
.await?;
self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block_ssz)
.await
}
/// `GET validator/attestation_data?slot,committee_index` /// `GET validator/attestation_data?slot,committee_index`
pub async fn get_validator_attestation_data( pub async fn get_validator_attestation_data(
&self, &self,

View File

@ -82,6 +82,7 @@ const HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4;
const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4;
const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4;
const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4; const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4;
const HTTP_GET_VALIDATOR_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4;
const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger"; const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger";
@ -309,6 +310,8 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
/ HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT, / HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT,
get_debug_beacon_states: slot_duration / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, get_debug_beacon_states: slot_duration / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT,
get_deposit_snapshot: slot_duration / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, get_deposit_snapshot: slot_duration / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT,
get_validator_block_ssz: slot_duration
/ HTTP_GET_VALIDATOR_BLOCK_SSZ_TIMEOUT_QUOTIENT,
} }
} else { } else {
Timeouts::set_all(slot_duration) Timeouts::set_all(slot_duration)