Skip to content

feat: add Electra support to beacon bridge #1803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions bin/portal-bridge/src/api/consensus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ use alloy::primitives::B256;
use anyhow::{anyhow, bail};
use constants::DEFAULT_BEACON_STATE_REQUEST_TIMEOUT;
use ethportal_api::{
consensus::beacon_state::BeaconStateDeneb,
consensus::beacon_state::BeaconState,
light_client::{
bootstrap::LightClientBootstrapDeneb, finality_update::LightClientFinalityUpdateDeneb,
optimistic_update::LightClientOptimisticUpdateDeneb, update::LightClientUpdateDeneb,
bootstrap::LightClientBootstrap, finality_update::LightClientFinalityUpdate,
optimistic_update::LightClientOptimisticUpdate, update::LightClientUpdate,
},
};
use reqwest::{
header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE},
Response,
};
use rpc_types::{RootResponse, VersionResponse, VersionedDataResponse, VersionedDataResult};
use rpc_types::{
RootResponse, VersionResponse, VersionedDataResponse, VersionedDataResult, VersionedDecode,
};
use serde::de::DeserializeOwned;
use ssz::Decode;
use tracing::{debug, warn};
use url::Url;

Expand Down Expand Up @@ -72,7 +73,7 @@ impl ConsensusApi {
pub async fn get_light_client_bootstrap(
&self,
block_root: B256,
) -> anyhow::Result<LightClientBootstrapDeneb> {
) -> anyhow::Result<LightClientBootstrap> {
let endpoint = format!("/eth/v1/beacon/light_client/bootstrap/{block_root}");
Ok(self.request(endpoint, None).await?.data)
}
Expand All @@ -95,7 +96,7 @@ impl ConsensusApi {
&self,
start_period: u64,
count: u64,
) -> anyhow::Result<Vec<LightClientUpdateDeneb>> {
) -> anyhow::Result<Vec<LightClientUpdate>> {
let endpoint = format!(
"/eth/v1/beacon/light_client/updates?start_period={start_period}&count={count}"
);
Expand All @@ -110,15 +111,15 @@ impl ConsensusApi {
/// Requests the latest `LightClientOptimisticUpdate` known by the server.
pub async fn get_light_client_optimistic_update(
&self,
) -> anyhow::Result<LightClientOptimisticUpdateDeneb> {
) -> anyhow::Result<LightClientOptimisticUpdate> {
let endpoint = "/eth/v1/beacon/light_client/optimistic_update".to_string();
Ok(self.request(endpoint, None).await?.data)
}

/// Requests the latest `LightClientFinalityUpdate` known by the server.
pub async fn get_light_client_finality_update(
&self,
) -> anyhow::Result<LightClientFinalityUpdateDeneb> {
) -> anyhow::Result<LightClientFinalityUpdate> {
let endpoint = "/eth/v1/beacon/light_client/finality_update".to_string();
Ok(self.request(endpoint, None).await?.data)
}
Expand All @@ -130,7 +131,7 @@ impl ConsensusApi {
}

/// Requests the `BeaconState` structure corresponding to the current head of the beacon chain.
pub async fn get_beacon_state(&self) -> anyhow::Result<BeaconStateDeneb> {
pub async fn get_beacon_state(&self) -> anyhow::Result<BeaconState> {
let endpoint = "/eth/v2/debug/beacon/states/finalized".to_string();
Ok(self
.request(endpoint, Some(DEFAULT_BEACON_STATE_REQUEST_TIMEOUT))
Expand All @@ -146,7 +147,7 @@ impl ConsensusApi {
custom_timeout: Option<Duration>,
) -> anyhow::Result<VersionedDataResponse<T>>
where
T: Decode + DeserializeOwned + Clone,
T: VersionedDecode + DeserializeOwned + Clone,
{
match Self::request_no_fallback(endpoint.clone(), &self.primary, custom_timeout).await {
Ok(response) => Ok(response),
Expand All @@ -171,7 +172,7 @@ impl ConsensusApi {
custom_timeout: Option<Duration>,
) -> anyhow::Result<Vec<VersionedDataResponse<T>>>
where
T: Decode + DeserializeOwned + Clone,
T: DeserializeOwned + Clone,
{
match Self::request_list_no_fallback(endpoint.clone(), &self.primary, custom_timeout).await
{
Expand Down Expand Up @@ -230,7 +231,7 @@ impl ConsensusApi {
custom_timeout: Option<Duration>,
) -> anyhow::Result<VersionedDataResponse<T>>
where
T: Decode + DeserializeOwned + Clone,
T: VersionedDecode + DeserializeOwned + Clone,
{
let (response, content_type) =
Self::base_request(endpoint.clone(), client, custom_timeout, false).await?;
Expand All @@ -246,8 +247,7 @@ impl ConsensusApi {
})
.transpose()?;
VersionedDataResponse::new(
T::from_ssz_bytes(&response.bytes().await?)
.map_err(|err| anyhow!("Failed to decode {err:?}"))?,
T::decode(version.as_deref(), &response.bytes().await?)?,
version,
)
}
Expand All @@ -264,7 +264,7 @@ impl ConsensusApi {
custom_timeout: Option<Duration>,
) -> anyhow::Result<Vec<VersionedDataResponse<T>>>
where
T: Decode + DeserializeOwned + Clone,
T: DeserializeOwned + Clone,
{
let (response, content_type) =
Self::base_request(endpoint.clone(), client, custom_timeout, true).await?;
Expand Down
87 changes: 86 additions & 1 deletion bin/portal-bridge/src/api/consensus/rpc_types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
use std::str::FromStr;

use alloy::primitives::{Bytes, B256};
use anyhow::bail;
use anyhow::{anyhow, bail};
use ethportal_api::{
consensus::{beacon_state::BeaconState, fork::ForkName},
light_client::{
bootstrap::LightClientBootstrap, finality_update::LightClientFinalityUpdate,
optimistic_update::LightClientOptimisticUpdate, update::LightClientUpdate,
},
};
use serde::Deserialize;
use serde_json::Value;
use ssz::Decode;
Expand Down Expand Up @@ -70,3 +79,79 @@ impl Decode for VersionResponse {
Ok(Self { version })
}
}

/// Trait that allows us to decode types based on provided version.
///
/// It's implemented by default for all types that implement [ssz::Decode], which simply ignores
/// the version.
pub trait VersionedDecode: Sized {
fn decode(version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self>;
}

impl VersionedDecode for RootResponse {
fn decode(_version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self> {
Self::from_ssz_bytes(bytes).map_err(|err| anyhow!("Error decoding RootResponse: {err:?}"))
}
}

impl VersionedDecode for VersionResponse {
fn decode(_version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self> {
let version = Bytes::from_ssz_bytes(bytes)
.map_err(|err| anyhow!("Error decoding VersionResponse: {err:?}"))?;
let version = String::from_utf8(version.to_vec())?;
Ok(Self { version })
}
}

impl VersionedDecode for LightClientBootstrap {
fn decode(version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self> {
let fork_name = fork_name_or_electra(version);
Self::from_ssz_bytes(bytes, fork_name).map_err(|err| {
anyhow!("Error decoding LightClientBootstrap (version: {version:?}), err: {err:?}")
})
}
}

impl VersionedDecode for LightClientUpdate {
fn decode(version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self> {
let fork_name = fork_name_or_electra(version);
Self::from_ssz_bytes(bytes, fork_name).map_err(|err| {
anyhow!("Error decoding LightClientUpdate (version: {version:?}), err: {err:?}")
})
}
}

impl VersionedDecode for LightClientFinalityUpdate {
fn decode(version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self> {
let fork_name = fork_name_or_electra(version);
Self::from_ssz_bytes(bytes, fork_name).map_err(|err| {
anyhow!("Error decoding LightClientFinalityUpdate (version: {version:?}), err: {err:?}")
})
}
}

impl VersionedDecode for LightClientOptimisticUpdate {
fn decode(version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self> {
let fork_name = fork_name_or_electra(version);
Self::from_ssz_bytes(bytes, fork_name).map_err(|err| {
anyhow!(
"Error decoding LightClientOptimisticUpdate (version: {version:?}), err: {err:?}"
)
})
}
}

impl VersionedDecode for BeaconState {
fn decode(version: Option<&str>, bytes: &[u8]) -> anyhow::Result<Self> {
let fork_name = fork_name_or_electra(version);
Self::from_ssz_bytes(bytes, fork_name).map_err(|err| {
anyhow!("Error decoding BeaconState (version: {version:?}), err: {err:?}")
})
}
}

fn fork_name_or_electra(version: Option<&str>) -> ForkName {
version
.and_then(|version| ForkName::from_str(version).ok())
.unwrap_or(ForkName::Electra)
}
89 changes: 47 additions & 42 deletions bin/portal-bridge/src/bridge/beacon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,21 @@ use std::{
};

use alloy::primitives::B256;
use anyhow::{bail, ensure};
use anyhow::bail;
use ethportal_api::{
consensus::historical_summaries::{
HistoricalSummariesStateProof, HistoricalSummariesWithProof,
consensus::{
beacon_state::BeaconState,
historical_summaries::{
HistoricalSummariesWithProof, HistoricalSummariesWithProofDeneb,
HistoricalSummariesWithProofElectra,
},
},
light_client::update::LightClientUpdate,
types::{
consensus::fork::ForkName,
content_key::beacon::{
HistoricalSummariesWithProofKey, LightClientFinalityUpdateKey,
LightClientOptimisticUpdateKey,
},
content_value::beacon::{
ForkVersionedHistoricalSummariesWithProof, ForkVersionedLightClientUpdate,
LightClientUpdatesByRange,
},
content_value::beacon::LightClientUpdatesByRange,
network::Subnetwork,
portal_wire::OfferTrace,
},
Expand All @@ -35,6 +34,7 @@ use tokio::{
use tracing::{error, info, warn, Instrument};
use trin_beacon::network::BeaconNetwork;
use trin_metrics::bridge::BridgeMetricsReporter;
use trin_validation::constants::SLOTS_PER_EPOCH;

use super::{constants::SERVE_BLOCK_TIMEOUT, offer_report::OfferReport};
use crate::{
Expand All @@ -45,12 +45,8 @@ use crate::{
utils::{duration_until_next_update, expected_current_slot},
};

/// The number of slots in an epoch.
const SLOTS_PER_EPOCH: u64 = 32;
/// The number of slots in a sync committee period.
const SLOTS_PER_PERIOD: u64 = SLOTS_PER_EPOCH * 256;
/// The historical summaries proof always has a length of 5 hashes.
const HISTORICAL_SUMMARIES_PROOF_LENGTH: usize = 5;

/// A helper struct to hold the finalized beacon state metadata.
#[derive(Clone, Debug, Default)]
Expand Down Expand Up @@ -298,7 +294,7 @@ impl BeaconBridge {
.await?;

info!(
header_slot=%bootstrap.header.beacon.slot,
header_slot=%bootstrap.get_beacon_block_header().slot,
"Generated LightClientBootstrap",
);

Expand Down Expand Up @@ -340,10 +336,12 @@ impl BeaconBridge {
}
}

let update = &consensus_api
let update = consensus_api
.get_light_client_updates(expected_current_period, 1)
.await?[0];
let finalized_header_period = update.finalized_header.beacon.slot / SLOTS_PER_PERIOD;
.await?
.remove(0);
let finalized_header_period =
update.finalized_beacon_block_header().slot / SLOTS_PER_PERIOD;

// We don't serve a `LightClientUpdate` if its finalized header slot is not within the
// expected current period.
Expand All @@ -356,10 +354,7 @@ impl BeaconBridge {
return Ok(());
}

let fork_versioned_update = ForkVersionedLightClientUpdate {
fork_name: ForkName::Deneb,
update: LightClientUpdate::Deneb(update.clone()),
};
let fork_versioned_update = update.into();

let content_value = BeaconContentValue::LightClientUpdatesByRange(
LightClientUpdatesByRange(VariableList::from(vec![fork_versioned_update])),
Expand Down Expand Up @@ -389,11 +384,11 @@ impl BeaconBridge {
) -> anyhow::Result<()> {
let update = consensus_api.get_light_client_optimistic_update().await?;
info!(
signature_slot = %update.signature_slot,
signature_slot = %update.signature_slot(),
"Generated LightClientOptimisticUpdate",
);
let content_key = BeaconContentKey::LightClientOptimisticUpdate(
LightClientOptimisticUpdateKey::new(update.signature_slot),
LightClientOptimisticUpdateKey::new(*update.signature_slot()),
);
let content_value = BeaconContentValue::LightClientOptimisticUpdate(update.into());
Self::spawn_offer_tasks(beacon_network, content_key, content_value, metrics, census);
Expand All @@ -408,11 +403,11 @@ impl BeaconBridge {
census: Census,
) -> anyhow::Result<()> {
let update = consensus_api.get_light_client_finality_update().await?;
let new_finalized_slot = update.finalized_beacon_block_header().slot;
info!(
finalized_slot = %update.finalized_header.beacon.slot,
finalized_slot = new_finalized_slot,
"Generated LightClientFinalityUpdate",
);
let new_finalized_slot = update.finalized_header.beacon.slot;

match new_finalized_slot.cmp(&*finalized_slot.lock().await) {
Ordering::Equal => {
Expand Down Expand Up @@ -470,21 +465,30 @@ impl BeaconBridge {
info!("Downloading beacon state for HistoricalSummariesWithProof generation...");
finalized_state_root.lock().await.in_progress = true;
let beacon_state = consensus_api.get_beacon_state().await?;
let state_epoch = beacon_state.slot / SLOTS_PER_EPOCH;
let historical_summaries_proof = beacon_state.build_historical_summaries_proof();
// Ensure the historical summaries proof is of the correct length
ensure!(
historical_summaries_proof.len() == HISTORICAL_SUMMARIES_PROOF_LENGTH,
"Historical summaries proof length is not 5"
);
let historical_summaries = beacon_state.historical_summaries;
let historical_summaries_with_proof = ForkVersionedHistoricalSummariesWithProof {
fork_name: ForkName::Deneb,
historical_summaries_with_proof: HistoricalSummariesWithProof {
epoch: state_epoch,
historical_summaries,
proof: HistoricalSummariesStateProof::from(historical_summaries_proof),
},
let state_epoch = beacon_state.slot() / SLOTS_PER_EPOCH;
let historical_summaries_with_proof = match beacon_state {
BeaconState::Bellatrix(_) => {
bail!("Unexpected Bellatrix BeaconState while serving historical summaries")
}
BeaconState::Capella(_) => {
bail!("Unexpected Capella BeaconState while serving historical summaries")
}
BeaconState::Deneb(beacon_state) => {
let historical_summaries_proof = beacon_state.build_historical_summaries_proof();
HistoricalSummariesWithProof::Deneb(HistoricalSummariesWithProofDeneb {
epoch: state_epoch,
historical_summaries: beacon_state.historical_summaries,
proof: historical_summaries_proof,
})
}
BeaconState::Electra(beacon_state) => {
let historical_summaries_proof = beacon_state.build_historical_summaries_proof();
HistoricalSummariesWithProof::Electra(HistoricalSummariesWithProofElectra {
epoch: state_epoch,
historical_summaries: beacon_state.historical_summaries,
proof: historical_summaries_proof,
})
}
};
info!(
epoch = %state_epoch,
Expand All @@ -494,8 +498,9 @@ impl BeaconBridge {
BeaconContentKey::HistoricalSummariesWithProof(HistoricalSummariesWithProofKey {
epoch: state_epoch,
});
let content_value =
BeaconContentValue::HistoricalSummariesWithProof(historical_summaries_with_proof);
let content_value = BeaconContentValue::HistoricalSummariesWithProof(
historical_summaries_with_proof.into(),
);

Self::spawn_offer_tasks(beacon_network, content_key, content_value, metrics, census);
finalized_state_root.lock().await.state_root = latest_finalized_state_root;
Expand Down
Loading
Loading