Skip to content

Commit

Permalink
Merge pull request #280 from ralexstokes/serde-poly
Browse files Browse the repository at this point in the history
use polymorphic types in beacon-api-client
  • Loading branch information
ralexstokes authored Oct 9, 2023
2 parents fcb7cf5 + 22ebbab commit 656674c
Show file tree
Hide file tree
Showing 19 changed files with 837 additions and 80 deletions.
29 changes: 18 additions & 11 deletions beacon-api-client/examples/api_sketch.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use beacon_api_client::{ApiError, ApiResult, Value, VersionedValue};
use ethereum_consensus::{bellatrix::mainnet as bellatrix, capella::mainnet as capella};
use ethereum_consensus::{
bellatrix::mainnet as bellatrix, state_transition::Forks, types::mainnet::BlindedBeaconBlock,
};
use std::collections::HashMap;

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(tag = "version", content = "data")]
#[serde(rename_all = "lowercase")]
enum BlindedBeaconBlock {
Bellatrix(bellatrix::BlindedBeaconBlock),
Capella(capella::BlindedBeaconBlock),
fn with_fork<T: serde::Serialize>(fork: Forks, value: T) -> serde_json::Value {
serde_json::json!( {
"version": fork,
"data": value,
})
}

fn main() {
Expand All @@ -24,17 +25,22 @@ fn main() {
println!("{block_with_version_repr}");

let block = BlindedBeaconBlock::Bellatrix(Default::default());
let block_with_version_repr = serde_json::to_string(&block).unwrap();
let block_with_version_repr =
serde_json::to_string(&with_fork(Forks::Bellatrix, &block)).unwrap();
println!("{block_with_version_repr}");
let recovered_block: BlindedBeaconBlock =
let recovered_block: VersionedValue<BlindedBeaconBlock> =
serde_json::from_str(&block_with_version_repr).unwrap();
println!("{recovered_block:#?}");

let block = BlindedBeaconBlock::Capella(Default::default());
let block_with_version_repr = serde_json::to_string(&block).unwrap();
println!("{block_with_version_repr}");

let full_success_response = ApiResult::Ok(block.clone());
let full_success_response = ApiResult::Ok(VersionedValue {
version: Forks::Capella,
data: block.clone(),
meta: Default::default(),
});
let str_repr = serde_json::to_string(&full_success_response).unwrap();
println!("{str_repr}");

Expand All @@ -43,7 +49,8 @@ fn main() {
println!("{recovered_success:#?}");

let full_success_response = ApiResult::Ok(VersionedValue {
payload: block,
version: Forks::Capella,
data: block,
meta: HashMap::from_iter([(
String::from("finalized_root"),
serde_json::Value::String("0xdeadbeefcafe".to_string()),
Expand Down
12 changes: 6 additions & 6 deletions beacon-api-client/src/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
HealthStatus, NetworkIdentity, PeerDescription, PeerState, PeerSummary, ProposerDuty,
PublicKeyOrIndex, RootData, StateId, SyncCommitteeDescriptor, SyncCommitteeDuty,
SyncCommitteeSummary, SyncStatus, ValidatorLiveness, ValidatorStatus, ValidatorSummary,
Value, VersionData,
Value, VersionData, VersionedValue,
},
ApiError, Error,
};
Expand Down Expand Up @@ -370,7 +370,7 @@ impl<

// v2 endpoint
pub async fn get_beacon_block(&self, id: BlockId) -> Result<SignedBeaconBlock, Error> {
let result: Value<SignedBeaconBlock> =
let result: VersionedValue<SignedBeaconBlock> =
self.get(&format!("eth/v2/beacon/blocks/{id}")).await?;
Ok(result.data)
}
Expand Down Expand Up @@ -414,7 +414,7 @@ impl<
}

pub async fn get_blinded_block(&self, id: BlockId) -> Result<SignedBlindedBeaconBlock, Error> {
let result: Value<SignedBlindedBeaconBlock> =
let result: VersionedValue<SignedBlindedBeaconBlock> =
self.get(&format!("eth/v1/beacon/blinded_blocks/{id}")).await?;
Ok(result.data)
}
Expand Down Expand Up @@ -586,7 +586,7 @@ impl<
/* debug namespace */
// v2 endpoint
pub async fn get_state(&self, id: StateId) -> Result<BeaconState, Error> {
let result: Value<BeaconState> =
let result: VersionedValue<BeaconState> =
self.get(&format!("eth/v2/debug/beacon/states/{id}")).await?;
Ok(result.data)
}
Expand Down Expand Up @@ -724,7 +724,7 @@ impl<
request = request.query(&[("graffiti", graffiti)]);
}
let response = request.send().await?;
let result: ApiResult<Value<BeaconBlock>> = response.json().await?;
let result: ApiResult<VersionedValue<BeaconBlock>> = response.json().await?;
match result {
ApiResult::Ok(result) => Ok(result.data),
ApiResult::Err(err) => Err(err.into()),
Expand All @@ -745,7 +745,7 @@ impl<
request = request.query(&[("graffiti", graffiti)]);
}
let response = request.send().await?;
let result: ApiResult<Value<BlindedBeaconBlock>> = response.json().await?;
let result: ApiResult<VersionedValue<BlindedBeaconBlock>> = response.json().await?;
match result {
ApiResult::Ok(result) => Ok(result.data),
ApiResult::Err(err) => Err(err.into()),
Expand Down
34 changes: 22 additions & 12 deletions beacon-api-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,26 @@ mod error {
pub mod presets {
pub mod mainnet {
use ethereum_consensus::{
altair::mainnet as altair, bellatrix::mainnet as bellatrix, deneb::mainnet as deneb,
altair::mainnet as altair,
deneb::mainnet as deneb,
phase0::mainnet as phase0,
types::mainnet::{
BeaconBlock, BeaconState, BlindedBeaconBlock, SignedBeaconBlock,
SignedBlindedBeaconBlock,
},
};

pub type Client = crate::Client<
altair::SignedContributionAndProof,
altair::SyncCommitteeContribution,
bellatrix::BlindedBeaconBlock,
bellatrix::SignedBlindedBeaconBlock,
BlindedBeaconBlock,
SignedBlindedBeaconBlock,
phase0::Attestation,
phase0::AttesterSlashing,
phase0::BeaconBlock,
phase0::BeaconState,
BeaconBlock,
BeaconState,
phase0::SignedAggregateAndProof,
phase0::SignedBeaconBlock,
SignedBeaconBlock,
deneb::BlobSidecar,
altair::LightClientBootstrap,
altair::LightClientUpdate,
Expand All @@ -58,21 +63,26 @@ pub mod presets {
}
pub mod minimal {
use ethereum_consensus::{
altair::minimal as altair, bellatrix::minimal as bellatrix, deneb::minimal as deneb,
altair::minimal as altair,
deneb::minimal as deneb,
phase0::minimal as phase0,
types::minimal::{
BeaconBlock, BeaconState, BlindedBeaconBlock, SignedBeaconBlock,
SignedBlindedBeaconBlock,
},
};

pub type Client = crate::Client<
altair::SignedContributionAndProof,
altair::SyncCommitteeContribution,
bellatrix::BlindedBeaconBlock,
bellatrix::SignedBlindedBeaconBlock,
BlindedBeaconBlock,
SignedBlindedBeaconBlock,
phase0::Attestation,
phase0::AttesterSlashing,
phase0::BeaconBlock,
phase0::BeaconState,
BeaconBlock,
BeaconState,
phase0::SignedAggregateAndProof,
phase0::SignedBeaconBlock,
SignedBeaconBlock,
deneb::BlobSidecar,
altair::LightClientBootstrap,
altair::LightClientUpdate,
Expand Down
143 changes: 121 additions & 22 deletions beacon-api-client/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ use ethereum_consensus::{
Root, Slot, ValidatorIndex, Version,
},
serde::try_bytes_from_hex_str,
state_transition::Forks,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{collections::HashMap, fmt, str::FromStr};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt, marker::PhantomData, str::FromStr};

#[derive(Serialize, Deserialize)]
pub struct VersionData {
Expand Down Expand Up @@ -457,38 +458,136 @@ pub struct ValidatorLiveness {
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")]
pub struct Value<T: Serialize + DeserializeOwned> {
#[serde(bound = "T: serde::Serialize + serde::de::DeserializeOwned")]
pub struct Value<T> {
pub data: T,
#[serde(flatten)]
pub meta: HashMap<String, serde_json::Value>,
}

/*
`VersionedValue` captures:
```json
{
"version": "fork-version",
"data": { ... },
< optional additional metadata >,
}
And can be combined with Rust `enum`s to handle polymorphic {de,}serialization.
```
*/
#[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(bound = "T: serde::Serialize + serde::de::DeserializeOwned")]
#[derive(Debug, Serialize)]
pub struct VersionedValue<T: serde::Serialize + serde::de::DeserializeOwned> {
#[serde(flatten)]
pub payload: T,
#[serde(flatten)]
pub version: Forks,
pub data: T,
pub meta: HashMap<String, serde_json::Value>,
}

impl<'de, T: serde::Serialize + serde::de::DeserializeOwned> serde::Deserialize<'de>
for VersionedValue<T>
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Debug)]
enum Field<'de> {
Version,
Data,
Meta(&'de str),
}

impl<'de> serde::Deserialize<'de> for Field<'de> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct FieldVisitor;

impl<'de> serde::de::Visitor<'de> for FieldVisitor {
type Value = Field<'de>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("some field name")
}

fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match v {
"version" => Ok(Field::Version),
"data" => Ok(Field::Data),
s => Ok(Field::Meta(s)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}

struct Visitor<T>(PhantomData<T>);

impl<'de, T: serde::Serialize + serde::de::DeserializeOwned> serde::de::Visitor<'de>
for Visitor<T>
{
type Value = VersionedValue<T>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("struct VersionedValue")
}

fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut version = None;
let mut version_str = None;
let mut data: Option<serde_json::Value> = None;
let mut meta = HashMap::default();
while let Some(key) = map.next_key()? {
match key {
Field::Version => {
if version.is_some() {
return Err(serde::de::Error::duplicate_field("version"))
}
let version_value: serde_json::Value = map.next_value()?;
let fork: Forks = serde_json::from_value(version_value.clone())
.map_err(serde::de::Error::custom)?;
version = Some(fork);
match version_value {
serde_json::Value::String(inner) => {
version_str = Some(inner);
}
other => {
return Err(serde::de::Error::custom(format!(
"expected JSON string, but found value {other}"
)))
}
};
}
Field::Data => {
if data.is_some() {
return Err(serde::de::Error::duplicate_field("data"))
}
data = Some(map.next_value()?);
}
Field::Meta(name) => {
let next_value: serde_json::Value = map.next_value()?;
meta.insert(name.to_string(), next_value);
}
}
}
let version = version.ok_or_else(|| serde::de::Error::missing_field("version"))?;
let data = data.ok_or_else(|| serde::de::Error::missing_field("data"))?;
let data_with_version = serde_json::json!({
"version": version_str,
"data": data,
});
let data: T =
serde_json::from_value(data_with_version).map_err(serde::de::Error::custom)?;
Ok(VersionedValue { version, data, meta })
}
}

const FIELDS: &[&str] = &["version", "data", "meta"];
deserializer.deserialize_struct("VersionedValue", FIELDS, Visitor(PhantomData))
}
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")]
#[serde(untagged)]
pub enum ApiResult<T: Serialize + DeserializeOwned> {
pub enum ApiResult<T> {
Ok(T),
Err(ApiError),
}
3 changes: 2 additions & 1 deletion ethereum-consensus/src/state_transition/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use crate::{
state_transition::Error,
};

#[derive(Debug)]
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Forks {
Phase0,
Altair,
Expand Down
Loading

0 comments on commit 656674c

Please sign in to comment.