Skip to content
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

[BUILDER] Implement Builder API consumer #2541

Merged
merged 4 commits into from
Mar 15, 2024
Merged
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
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