-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[BUILDER] Implement Builder API consumer (#2541)
* Implement Builder API consumer * Update to latest hs-builder-api
- Loading branch information
Showing
9 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.