Skip to content

Commit

Permalink
[BUILDER] Implement Builder API consumer (#2541)
Browse files Browse the repository at this point in the history
* Implement Builder API consumer

* Update to latest hs-builder-api
  • Loading branch information
QuentinI authored Mar 15, 2024
1 parent 00681b2 commit 9a5e1de
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 0 deletions.
25 changes: 25 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ jf-primitives = { git = "https://github.com/EspressoSystems/jellyfish", tag = "0
jf-plonk = { git = "https://github.com/EspressoSystems/jellyfish", tag = "0.4.1" }
jf-relation = { git = "https://github.com/EspressoSystems/jellyfish", tag = "0.4.1" }
jf-utils = { git = "https://github.com/espressosystems/jellyfish", tag = "0.4.1" }
# TODO: point to main when HotShot catches up to the same hotshot-types version as hs-builder-api
hs-builder-api = { git = "https://github.com/EspressoSystems/hs-builder-api", branch = "ag/types-0.1.2" }
lazy_static = "1.4.0"
libp2p-identity = "0.2"
libp2p-networking = { path = "./crates/libp2p-networking", version = "0.1.0", default-features = false }
libp2p-swarm-derive = { version = "0.34.1" }
portpicker = "0.1.1"
rand = "0.8.5"
rand_chacha = { version = "0.3.1", default-features = false }
serde = { version = "1.0.197", features = ["derive"] }
Expand Down
4 changes: 4 additions & 0 deletions crates/task-impls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ async-lock = { workspace = true }
tracing = { workspace = true }
hotshot-types = { workspace = true }
hotshot-utils = { path = "../utils" }
hs-builder-api = { workspace = true }
jf-primitives = { workspace = true }
time = { workspace = true }
commit = { workspace = true }
Expand All @@ -24,6 +25,9 @@ sha2 = { workspace = true }
hotshot-task = { path = "../task" }
async-broadcast = { workspace = true }
chrono = "0.4"
surf-disco = { workspace = true }
serde = { workspace = true }
tagged-base64 = { workspace = true }

[target.'cfg(all(async_executor_impl = "tokio"))'.dependencies]
tokio = { workspace = true }
Expand Down
127 changes: 127 additions & 0 deletions crates/task-impls/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use async_compatibility_layer::art::async_sleep;
use std::time::{Duration, Instant};

use hotshot_types::{
traits::{node_implementation::NodeType, signature_key::SignatureKey},
utils::BuilderCommitment,
vid::VidCommitment,
};
use hs_builder_api::builder::{BuildError, Error as BuilderApiError};
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use surf_disco::{client::HealthStatus, Client, Url};
use tagged_base64::TaggedBase64;

#[derive(Debug, Snafu, Serialize, Deserialize)]
/// Represents errors thant builder client may return
pub enum BuilderClientError {
// NOTE: folds BuilderError::NotFound & builderError::Missing
// into one. Maybe we'll want to handle that separately in
// the future
/// Block not found
#[snafu(display("Requested block not found"))]
NotFound,
/// Generic error while accessing the API,
/// i.e. when API isn't available or compatible
#[snafu(display("Builder API error: {message}"))]
Api {
/// Underlying error
message: String,
},
}

impl From<BuilderApiError> for BuilderClientError {
fn from(value: BuilderApiError) -> Self {
match value {
BuilderApiError::Request { source } | BuilderApiError::TxnUnpack { source } => {
Self::Api {
message: source.to_string(),
}
}
BuilderApiError::TxnSubmit { source } => Self::Api {
message: source.to_string(),
},
BuilderApiError::Custom { message, .. } => Self::Api { message },
BuilderApiError::BlockAvailable { source, .. }
| BuilderApiError::BlockClaim { source, .. } => match source {
BuildError::NotFound | BuildError::Missing => Self::NotFound,
BuildError::Error { message } => Self::Api { message },
},
}
}
}

/// Client for builder API
pub struct BuilderClient<TYPES: NodeType> {
/// Underlying surf_disco::Client
inner: Client<BuilderApiError>,
/// Marker for [`NodeType`] used here
_marker: std::marker::PhantomData<TYPES>,
}

impl<TYPES: NodeType> BuilderClient<TYPES>
where
<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType:
for<'a> TryFrom<&'a TaggedBase64> + Into<TaggedBase64>,
{
/// Construct a new client from base url
pub fn new(base_url: impl Into<Url>) -> Self {
Self {
inner: Client::new(base_url.into()),
_marker: std::marker::PhantomData,
}
}

/// Wait for server to become available
/// Returns `false` if server doesn't respond
/// with OK healthcheck before `timeout`
pub async fn connect(&self, timeout: Duration) -> bool {
let timeout = Instant::now() + timeout;
let mut backoff = Duration::from_millis(50);
while Instant::now() < timeout {
if matches!(
self.inner.healthcheck::<HealthStatus>().await,
Ok(HealthStatus::Available)
) {
return true;
}
async_sleep(backoff).await;
backoff *= 2;
}
false
}

/// Query builder for available blocks
///
/// # Errors
/// - [`BuilderClientError::NotFound`] if blocks aren't available for this parent
/// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
pub async fn get_avaliable_blocks(
&self,
parent: VidCommitment,
) -> Result<Vec<BuilderCommitment>, BuilderClientError> {
self.inner
.get(&format!("availableblocks/{parent}"))
.send()
.await
.map_err(Into::into)
}

/// Claim block
///
/// # Errors
/// - [`BuilderClientError::NotFound`] if block isn't available
/// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
pub async fn claim_block(
&self,
block_hash: BuilderCommitment,
signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
) -> Result<TYPES::BlockPayload, BuilderClientError> {
let encoded_signature: TaggedBase64 = signature.clone().into();
self.inner
.get(&format!("claimblock/{block_hash}/{encoded_signature}"))
.send()
.await
.map_err(Into::into)
}
}
4 changes: 4 additions & 0 deletions crates/task-impls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub mod vote;
/// Task for handling upgrades
pub mod upgrade;

/// Implementations for builder client
/// Should contain builder task in the future
pub mod builder;

/// Helper functions used by any task
pub mod helpers;

Expand Down
4 changes: 4 additions & 0 deletions crates/testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ slow-tests = []
[dependencies]
async-broadcast = { workspace = true }
async-compatibility-layer = { workspace = true }
async-trait = { workspace = true }
sha3 = "^0.10"
bincode = { workspace = true }
commit = { workspace = true }
Expand All @@ -24,6 +25,7 @@ hotshot-utils = { path = "../utils" }
hotshot-macros = { path = "../macros" }
hotshot-orchestrator = { version = "0.1.1", path = "../orchestrator", default-features = false }
hotshot-task-impls = { path = "../task-impls", version = "0.1.0", default-features = false }
hs-builder-api = { workspace = true }
jf-primitives = { workspace = true }
rand = { workspace = true }
snafu = { workspace = true }
Expand All @@ -35,6 +37,8 @@ bitvec = { workspace = true }
ethereum-types = { workspace = true }
hotshot-task = { path = "../task" }
hotshot-example-types = { path = "../example-types" }
tide-disco = { workspace = true }
portpicker = { workspace = true }

[target.'cfg(all(async_executor_impl = "tokio"))'.dependencies]
tokio = { workspace = true }
Expand Down
124 changes: 124 additions & 0 deletions crates/testing/src/block_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use async_compatibility_layer::art::async_spawn;
use async_trait::async_trait;
use futures::future::BoxFuture;
use hotshot::traits::BlockPayload;
use hotshot::types::SignatureKey;
use hotshot_example_types::{block_types::TestBlockPayload, node_types::TestTypes};
use hotshot_types::traits::block_contents::vid_commitment;
use hotshot_types::utils::BuilderCommitment;
use hotshot_types::{traits::node_implementation::NodeType, vid::VidCommitment};
use hs_builder_api::block_info::{
AvailableBlockData, AvailableBlockHeaderInput, AvailableBlockInfo,
};
use hs_builder_api::{
builder::{BuildError, Options},
data_source::BuilderDataSource,
};
use tide_disco::{method::ReadState, App, Url};

/// The only block [`TestableBuilderSource`] provides
const EMPTY_BLOCK: TestBlockPayload = TestBlockPayload {
transactions: vec![],
};

/// A mock implementation of the builder data source.
/// "Builds" only empty blocks.
pub struct TestableBuilderSource {
priv_key: <<TestTypes as NodeType>::SignatureKey as SignatureKey>::PrivateKey,
pub_key: <TestTypes as NodeType>::SignatureKey,
}

#[async_trait]
impl ReadState for TestableBuilderSource {
type State = Self;

async fn read<T>(
&self,
op: impl Send + for<'a> FnOnce(&'a Self::State) -> BoxFuture<'a, T> + 'async_trait,
) -> T {
op(self).await
}
}

#[async_trait]
impl BuilderDataSource<TestTypes> for TestableBuilderSource {
async fn get_available_blocks(
&self,
_for_parent: &VidCommitment,
) -> Result<Vec<AvailableBlockInfo<TestTypes>>, BuildError> {
Ok(vec![AvailableBlockInfo {
sender: self.pub_key,
signature: <TestTypes as NodeType>::SignatureKey::sign(
&self.priv_key,
EMPTY_BLOCK.builder_commitment(&()).as_ref(),
)
.unwrap(),
block_hash: EMPTY_BLOCK.builder_commitment(&()),
block_size: 0,
offered_fee: 1,
_phantom: std::marker::PhantomData,
}])
}

async fn claim_block(
&self,
block_hash: &BuilderCommitment,
_signature: &<<TestTypes as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
) -> Result<AvailableBlockData<TestTypes>, BuildError> {
if block_hash == &EMPTY_BLOCK.builder_commitment(&()) {
Ok(AvailableBlockData {
block_payload: EMPTY_BLOCK,
metadata: (),
signature: <TestTypes as NodeType>::SignatureKey::sign(
&self.priv_key,
EMPTY_BLOCK.builder_commitment(&()).as_ref(),
)
.unwrap(),
sender: self.pub_key,
_phantom: std::marker::PhantomData,
})
} else {
Err(BuildError::Missing)
}
}

async fn claim_block_header_input(
&self,
block_hash: &BuilderCommitment,
_signature: &<<TestTypes as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
) -> Result<AvailableBlockHeaderInput<TestTypes>, BuildError> {
if block_hash == &EMPTY_BLOCK.builder_commitment(&()) {
Ok(AvailableBlockHeaderInput {
vid_commitment: vid_commitment(&vec![], 1),
signature: <TestTypes as NodeType>::SignatureKey::sign(
&self.priv_key,
EMPTY_BLOCK.builder_commitment(&()).as_ref(),
)
.unwrap(),
sender: self.pub_key,
_phantom: std::marker::PhantomData,
})
} else {
Err(BuildError::Missing)
}
}
}

/// Construct a tide disco app that mocks the builder API.
///
/// # Panics
/// If constructing and launching the builder fails for any reason
pub fn run_builder(url: Url) {
let builder_api = hs_builder_api::builder::define_api::<TestableBuilderSource, TestTypes>(
&Options::default(),
)
.expect("Failed to construct the builder API");
let (pub_key, priv_key) =
<TestTypes as NodeType>::SignatureKey::generated_from_seed_indexed([1; 32], 0);
let mut app: App<TestableBuilderSource, hs_builder_api::builder::Error> =
App::with_state(TestableBuilderSource { priv_key, pub_key });
app.register_module("/", builder_api)
.expect("Failed to register the builder API");

async_spawn(app.serve(url));
}
3 changes: 3 additions & 0 deletions crates/testing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub mod spinning_task;
/// task for checking if view sync got activated
pub mod view_sync_task;

/// Test implementation of block builder
pub mod block_builder;

/// predicates to use in tests
pub mod predicates;

Expand Down
Loading

0 comments on commit 9a5e1de

Please sign in to comment.