Skip to content

Commit

Permalink
feat(derive): online blob provider
Browse files Browse the repository at this point in the history
  • Loading branch information
refcell committed Apr 16, 2024
1 parent a434571 commit 57daaef
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 1 deletion.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ reqwest = { version = "0.12", default-features = false, optional = true }
tokio = { version = "1.36", features = ["full"] }
proptest = "1.4.0"
tracing-subscriber = "0.3.18"
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "e3f2f07", default-features = false }
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "e3f2f07", default-features = false }

[features]
default = ["serde", "k256"]
Expand Down
4 changes: 3 additions & 1 deletion crates/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ pub mod traits;
pub mod types;

#[cfg(feature = "online")]
pub mod alloy_providers;
mod online;
#[cfg(feature = "online")]
pub use online::prelude::*;

/// The derivation pipeline is responsible for deriving L2 inputs from L1 data.
#[derive(Debug, Clone, Copy)]
Expand Down
File renamed without changes.
236 changes: 236 additions & 0 deletions crates/derive/src/online/blob_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
#![allow(dead_code)]
//! Contains an online implementation of the [BlobProvider] trait.
use crate::{
traits::BlobProvider,
types::{Blob, BlockInfo, IndexedBlobHash},
};
use alloc::{boxed::Box, vec::Vec};
use alloy_provider::Provider;
use alloy_transport_http::Http;
use anyhow::Result;
use async_trait::async_trait;
use reqwest::Client;
use tracing::debug;

const (
versionMethod = "eth/v1/node/version"
specMethod = "eth/v1/config/spec"
genesisMethod = "eth/v1/beacon/genesis"
sidecarsMethodPrefix = "eth/v1/beacon/blob_sidecars/"
)

/// The [BeaconClient] is a thin wrapper around the Beacon API.
pub trait BeaconClient {
/// Returns the node version.
fn node_version(&self) -> Result<String>;

/// Returns the config spec.
fn config_spec(&self) -> Result<APIConfigResponse>;

/// Returns the beacon genesis.
fn beacon_genesis(&self) -> Result<APIGenesisResponse>;

/// Fetches blob sidecars that were confirmed in the specified L1 block with the given indexed
/// hashes. Order of the returned sidecars is guaranteed to be that of the hashes. Blob data is
/// not checked for validity.
fn beacon_blob_side_cars(
&self,
fetch_all_sidecars: bool,
slot: u64,
hashes: Vec<IndexedBlobHash>,
) -> Result<APIGetBlobSidecarsResponse>;
}

/// Specifies the derivation of a slot from a timestamp.
pub trait SlotDerivation {
/// Converts a timestamp to a slot number.
fn slot(genesis: u64, slot_time: u64, timestamp: u64) -> Result<u64>;
}

/// An online implementation of the [BlobProvider] trait.
#[derive(Debug)]
pub struct OnlineBlobProvider<T: Provider<Http<Client>>, B: BeaconClient, S: SlotDerivation> {
/// The inner Ethereum JSON-RPC provider.
inner: T,
/// The Beacon API client.
beacon_client: B,
/// Beacon Genesis used for the time to slot conversion.
genesis: Option<BeaconGenesis>,
/// Config spec used for the time to slot conversion.
config_spec: Option<ConfigSpec>,
/// Phantom data for slot derivation.
_slot_derivation: PhantomData<S>,
}

impl<T: Provider<Http<Client>>, B: BeaconClient, S: SlotDerivation> OnlineBlobProvider<T, B, S> {
/// Creates a new instance of the [OnlineBlobProvider].
///
/// The `genesis` and `config_spec` arguments are _optional_ and the [OnlineBlockProvider]
/// will attempt to load them dynamically at runtime if they are not provided.
pub fn new(inner: T, beacon_client: B, genesis: Option<BeasonGenesis>, config_spec: Option<ConfigSpec>) -> Self {
Self { inner, beacon_client, genesis, config_spec }
}

/// Loads the beacon genesis and config spec
pub fn load_configs(&mut self) -> Result<()> {
if self.genesis.is_none() {
debug!("Loading missing BeaconGenesis");
self.genesis = Some(self.beacon_client.beacon_genesis()?);
}
if self.config_spec.is_none() {
debug!("Loading missing ConfigSpec");
self.config_spec = Some(self.beacon_client.config_spec()?);
}
Ok(())
}

/// Fetches blob sidecars for the given slot and blob hashes.
pub async fn fetch_sidecars(&self, slot: u64, hashes: Vec<IndexedBlobHash>) -> Result<BlobSidecar> {
unimplemented!("fetching blob sidecars is not implemented");
}


/// Fetches blob sidecars that were confirmed in the specified L1 block with the given indexed
/// hashes. Order of the returned sidecars is guaranteed to be that of the hashes. Blob data is
/// not checked for validity.
pub async fn get_blob_sidecars(
&mut self,
block_ref: &BlockInfo,
blob_hashes: Vec<IndexedBlobHash>,
) -> Result<Vec<Blob>> {
if blob_hashes.is_empty() {
return Ok(Vec::new());
}

// Fetches [BeaconGenesis] and [ConfigSpec] configs if not previously loaded.
self.load_configs()?;

// Extract the genesis timestamp and slot interval from the loaded configs.
let genesis = self.genesis.expect("Genesis Config Loaded").data.genesis_time;
let interval = self.config_spec.expect("Config Spec Loaded").data.seconds_per_slot;

// Calculate the slot for the given timestamp.
let slot = S::slot(genesis, interval, block_ref.timestamp)?;

// Fetch blob sidecars for the slot using the given blob hashes.
let sidecars = self.fetch_sidecars(slot, blob_hashes).await?;

let blob_hash_indicies = blob_hashes.iter().map(|b| b.index).collect::<Vec<_>>();
let filtered = sidecars.iter().filter(|s| blob_hashes.contains(s.index)).collect::<Vec<_>>();

if filtered.len() != blob_hashes.len() {

}

// TODO: implement
Ok(Vec::new())
}
}

#[async_trait]
impl<T: Provider<Http<Client>>> BlobProvider for OnlineBlobProvider<T> {
async fn get_blobs(
&self,
_block_ref: &BlockInfo,
_blob_hashes: Vec<IndexedBlobHash>,
) -> Result<Vec<Blob>> {
unimplemented!("TODO: Implement OnlineBlobProvider::get_blobs")
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::online::test_utils::spawn_anvil;

#[tokio::test]
async fn test_get_blob_sidecars_empty_hashes() {
let (provider, _anvil) = spawn_anvil();
let blob_provider = OnlineBlobProvider::new(provider);
let block_ref = BlockInfo::default();
let blob_hashes = Vec::new();
let result = blob_provider.get_blob_sidecars(&block_ref, blob_hashes).await;
assert!(result.unwrap().is_empty());
}
}

// GetTimeToSlotFn returns a function that converts a timestamp to a slot number.
func (cl *L1BeaconClient) GetTimeToSlotFn(ctx context.Context) (TimeToSlotFn, error) {
cl.initLock.Lock()
defer cl.initLock.Unlock()
if cl.timeToSlotFn != nil {
return cl.timeToSlotFn, nil
}

genesis, err := cl.cl.BeaconGenesis(ctx)
if err != nil {
return nil, err
}

config, err := cl.cl.ConfigSpec(ctx)
if err != nil {
return nil, err
}

genesisTime := uint64(genesis.Data.GenesisTime)
secondsPerSlot := uint64(config.Data.SecondsPerSlot)
if secondsPerSlot == 0 {
return nil, fmt.Errorf("got bad value for seconds per slot: %v", config.Data.SecondsPerSlot)
}
cl.timeToSlotFn = func(timestamp uint64) (uint64, error) {
if timestamp < genesisTime {
return 0, fmt.Errorf("provided timestamp (%v) precedes genesis time (%v)", timestamp, genesisTime)
}
return (timestamp - genesisTime) / secondsPerSlot, nil
}
return cl.timeToSlotFn, nil
}


// GetBlobSidecars fetches blob sidecars that were confirmed in the specified
// L1 block with the given indexed hashes.
// Order of the returned sidecars is guaranteed to be that of the hashes.
// Blob data is not checked for validity.

// func (cl *L1BeaconClient) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRef, hashes
// []eth.IndexedBlobHash) ([]*eth.BlobSidecar, error) { if len(hashes) == 0 {
// return []*eth.BlobSidecar{}, nil
// }

// slotFn, err := cl.GetTimeToSlotFn(ctx)
// if err != nil {
// return nil, fmt.Errorf("failed to get time to slot function: %w", err)
// }
// slot, err := slotFn(ref.Time)
// if err != nil {
// return nil, fmt.Errorf("error in converting ref.Time to slot: %w", err)
// }
//
// resp, err := cl.fetchSidecars(ctx, slot, hashes)
// if err != nil {
// return nil, fmt.Errorf("failed to fetch blob sidecars for slot %v block %v: %w", slot, ref, err)
// }
//
// apiscs := make([]*eth.APIBlobSidecar, 0, len(hashes))
// // filter and order by hashes
// for _, h := range hashes {
// for _, apisc := range resp.Data {
// if h.Index == uint64(apisc.Index) {
// apiscs = append(apiscs, apisc)
// break
// }
// }
// }
//
// if len(hashes) != len(apiscs) {
// return nil, fmt.Errorf("expected %v sidecars but got %v", len(hashes), len(apiscs))
// }
//
// bscs := make([]*eth.BlobSidecar, 0, len(hashes))
// for _, apisc := range apiscs {
// bscs = append(bscs, apisc.BlobSidecar())
// }
//
// return bscs, nil
// }
15 changes: 15 additions & 0 deletions crates/derive/src/online/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Contains "online" implementations for providers.
/// Prelude for online providers.
pub(crate) mod prelude {
pub use super::{AlloyChainProvider, AlloyL2ChainProvider, OnlineBlobProvider};
}

#[cfg(test)]
pub(crate) mod test_utils;

mod alloy_providers;
pub use alloy_providers::{AlloyChainProvider, AlloyL2ChainProvider};

mod blob_provider;
pub use blob_provider::OnlineBlobProvider;
22 changes: 22 additions & 0 deletions crates/derive/src/online/test_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Test Utilities for Online Providers
use alloy_node_bindings::{Anvil, AnvilInstance};
use alloy_provider::{network::Ethereum, ReqwestProvider};
use alloy_rpc_client::RpcClient;
use alloy_transport_http::Http;
use reqwest::Client;

pub(crate) fn spawn_anvil() -> (ReqwestProvider<Ethereum>, AnvilInstance) {
let anvil = Anvil::new().try_spawn().expect("could not spawn anvil");
(anvil_http_provider(&anvil), anvil)
}

pub(crate) fn anvil_http_provider(anvil: &AnvilInstance) -> ReqwestProvider<Ethereum> {
http_provider(&anvil.endpoint())
}

pub(crate) fn http_provider(url: &str) -> ReqwestProvider<Ethereum> {
let url = url.parse().unwrap();
let http = Http::<Client>::new(url);
ReqwestProvider::new(RpcClient::new(http, true))
}
7 changes: 7 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ set positional-arguments
default:
@just --list

# Run all tests
tests: test test-online test-docs

# Test for the native target
test *args='':
cargo nextest run --workspace --all $@

# Run online tests
test-online:
cargo nextest run --workspace --all --features online

# Lint the workspace for all available targets
lint: lint-native lint-cannon lint-asterisc lint-docs

Expand Down

0 comments on commit 57daaef

Please sign in to comment.