Expand description
The HotShot Query Service is a minimal, generic query service that can be integrated into any decentralized application running on the hotshot consensus layer. It provides all the features that HotShot itself expects of a query service (such as providing consensus-related data for catchup and synchronization) as well as some application-level features that deal only with consensus-related or application-agnostic data. In addition, the query service is provided as an extensible library, which makes it easy to add additional, application-specific features.
§Basic usage
use hotshot_query_service::{
availability,
data_source::{FileSystemDataSource, Transaction, UpdateDataSource, VersionedDataSource},
fetching::provider::NoFetching,
node,
status::UpdateStatusData,
status,
testing::mocks::MockBase,
ApiState, Error,
};
use futures::StreamExt;
use vbs::version::StaticVersionType;
use hotshot::SystemContext;
use std::sync::Arc;
use tide_disco::App;
use tokio::spawn;
// Create or open a data source.
let data_source = FileSystemDataSource::<AppTypes, NoFetching>::create(storage_path, NoFetching)
.await?;
// Create hotshot, giving it a handle to the status metrics.
let hotshot = SystemContext::<AppTypes, AppNodeImpl, AppVersions>::init(
ConsensusMetricsValue::new(&*data_source.populate_metrics()), panic!(),
panic!()
// Other fields omitted
).await?.0;
// Create API modules.
let availability_api = availability::define_api(&Default::default(), MockBase::instance())?;
let node_api = node::define_api(&Default::default(), MockBase::instance())?;
let status_api = status::define_api(&Default::default(), MockBase::instance())?;
// Create app.
let data_source = ApiState::from(data_source);
let mut app = App::<_, Error>::with_state(data_source.clone());
app
.register_module("availability", availability_api)?
.register_module("node", node_api)?
.register_module("status", status_api)?;
// Serve app.
spawn(app.serve("0.0.0.0:8080", MockBase::instance()));
// Update query data using HotShot events.
let mut events = hotshot.event_stream();
while let Some(event) = events.next().await {
// Update the query data based on this event.
data_source.update(&event).await.ok();
}
Shortcut for starting an out-of-the-box service with no extensions (does exactly the above and nothing more):
use hotshot_query_service::run_standalone_service;
let data_source = FileSystemDataSource::create(storage_path, NoFetching).await.map_err(Error::internal)?;
spawn(run_standalone_service(options, data_source, hotshot, MockBase::instance()));
§Persistence
Naturally, an archival query service such as this is heavily dependent on a persistent storage
implementation. The APIs provided by this query service are generic over the specific type of
the persistence layer, which we call a data source. This crate provides several data source
implementations in the data_source
module.
§Interaction with other components
While the HotShot Query Service can be used as a standalone service, it is designed to be used as a single component of a larger service consisting of several other interacting components. This interaction has two dimensions:
- extension, adding new functionality to the API modules provided by this crate
- composition, combining the API modules from this crate with other, application-specific API modules to create a single [tide_disco] API
§Extension
It is possible to add new functionality directly to the modules provided by this create. This allows you to keep semantically related functionality grouped together in a single API module, for interface purposes, even while some of the functionality of that module is provided by this crate and some of it is an application-specific extension.
For example, consider an application which is a UTXO-based blockchain. Each transaction consists of a handful of new output records, and you want your query service to provide an API for looking up a specific output by its index. Semantically, this functionality belongs in the data availability API, however it is application-specific – HotShot itself makes no assumptions and provides no guarantees about the internal structure of a transaction. In order to expose this UTXO-specific functionality as well as the generic data availability functionality provided by this crate as part of the same public API, you can extend the availability module of this crate with additional data structures and endpoints that know about the internal structure of your transactions.
There are two parts to adding additional functionality to a module in this crate: adding the required additional data structures to the data source, and creating a new API endpoint to expose the functionality. The mechanism for the former will depend on the specific data source you are using. Check the documentation for your data source implementation to see how it can be extended.
For the latter, you can modify the default availability API with the addition of a new endpoint that accesses the custom state you have added to the data source. It is good practice to define a trait for accessing this custom state, so that if you want to switch data sources in the future, you can easily extend the new data source, implement the trait, and then transparently replace the data source that you use to set up your API. In the case of adding a UTXO index, this trait might look like this:
use async_trait::async_trait;
#[async_trait]
trait UtxoDataSource: AvailabilityDataSource<AppTypes> {
// Index mapping UTXO index to (block index, transaction index, output index)
async fn find_utxo(&self, utxo: u64) -> Option<(usize, TransactionIndex<AppTypes>, usize)>;
}
Implement this trait for the extended data source you’re using, and then add a new endpoint to the availability API like so:
fn define_app_specific_availability_api<State, Ver: StaticVersionType + 'static>(
options: &availability::Options,
bind_version: Ver,
) -> Result<Api<State, availability::Error, Ver>, ApiError>
where
State: 'static + Send + Sync + ReadState,
<State as ReadState>::State: UtxoDataSource + Send + Sync,
{
let mut api = availability::define_api(options, bind_version)?;
api.get("get_utxo", |req, state: &<State as ReadState>::State| async move {
let utxo_index = req.integer_param("index")?;
let (block_index, txn_index, output_index) = state
.find_utxo(utxo_index)
.await
.ok_or_else(|| availability::Error::Custom {
message: format!("no such UTXO {}", utxo_index),
status: StatusCode::NOT_FOUND,
})?;
let block = state
.get_block(block_index)
.await
.context(FetchBlockSnafu { resource: block_index.to_string() })?;
let txn = block.transaction(&txn_index).unwrap();
let utxo = // Application-specific logic to extract a UTXO from a transaction.
Ok(utxo)
}.boxed())?;
Ok(api)
}
fn init_server<D: UtxoDataSource + Send + Sync + 'static, Ver: StaticVersionType + 'static>(
options: &availability::Options,
data_source: D,
bind_version: Ver,
) -> Result<App<ApiState<D>, Error>, availability::Error> {
let api = define_app_specific_availability_api(options, bind_version)
.map_err(availability::Error::internal)?;
let mut app = App::<_, _>::with_state(ApiState::from(data_source));
app.register_module("availability", api).map_err(availability::Error::internal)?;
Ok(app)
}
Now you need to define the new route, get_utxo
, in your API specification. Create a file
app_specific_availability.toml
:
[route.get_utxo]
PATH = ["utxo/:index"]
":index" = "Integer"
DOC = "Get a UTXO by its index"
and make sure options.extensions
includes "app_specific_availability.toml"
.
§Composition
Composing the modules provided by this crate with other, unrelated modules to create a unified service is fairly simple, as most of the complexity is handled by [tide_disco], which already provides a mechanism for composing several modules into a single application. In principle, all you need to do is register the availability, node, and status APIs provided by this crate with a [tide_disco::App], and then register your own API modules with the same app.
The one wrinkle is that all modules within a [tide_disco] app must share the same state type. It is for this reason that the modules provided by this crate are generic on the state type – availability::define_api, node::define_api, and status::define_api can all work with any state type, provided that type implements the corresponding data source traits. The data sources provided by this crate implement these traits, but if you want to use a custom state type that includes state for other modules, you will need to implement these traits for your custom type. The basic pattern looks like this:
// Our AppState takes an underlying data source `D` which already implements the relevant
// traits, and adds some state for use with other modules.
struct AppState<D> {
hotshot_qs: D,
// additional state for other modules
}
// Implement data source trait for availability API by delegating to the underlying data source.
#[async_trait]
impl<D: AvailabilityDataSource<AppTypes> + Send + Sync>
AvailabilityDataSource<AppTypes> for AppState<D>
{
async fn get_leaf<ID>(&self, id: ID) -> Fetch<LeafQueryData<AppTypes>>
where
ID: Into<LeafId<AppTypes>> + Send + Sync,
{
self.hotshot_qs.get_leaf(id).await
}
// etc
}
// Implement data source trait for node API by delegating to the underlying data source.
#[async_trait]
impl<D: NodeDataSource<AppTypes> + Send + Sync> NodeDataSource<AppTypes> for AppState<D> {
async fn block_height(&self) -> QueryResult<usize> {
self.hotshot_qs.block_height().await
}
async fn count_transactions_in_range(
&self,
range: impl RangeBounds<usize> + Send,
) -> QueryResult<usize> {
self.hotshot_qs.count_transactions_in_range(range).await
}
async fn payload_size_in_range(
&self,
range: impl RangeBounds<usize> + Send,
) -> QueryResult<usize> {
self.hotshot_qs.payload_size_in_range(range).await
}
async fn vid_share<ID>(&self, id: ID) -> QueryResult<VidShare>
where
ID: Into<BlockId<AppTypes>> + Send + Sync,
{
self.hotshot_qs.vid_share(id).await
}
async fn sync_status(&self) -> QueryResult<SyncStatus> {
self.hotshot_qs.sync_status().await
}
async fn get_header_window(
&self,
start: impl Into<WindowStart<AppTypes>> + Send + Sync,
end: u64,
limit: usize,
) -> QueryResult<TimeWindowQueryData<Header<AppTypes>>> {
self.hotshot_qs.get_header_window(start, end, limit).await
}
}
// Implement data source trait for status API by delegating to the underlying data source.
impl<D: HasMetrics> HasMetrics for AppState<D> {
fn metrics(&self) -> &PrometheusMetrics {
self.hotshot_qs.metrics()
}
}
#[async_trait]
impl<D: StatusDataSource + Send + Sync> StatusDataSource for AppState<D> {
async fn block_height(&self) -> QueryResult<usize> {
self.hotshot_qs.block_height().await
}
}
// Implement data source traits for other modules, using additional state from AppState.
In the future, we may provide derive macros for AvailabilityDataSource, NodeDataSource, and StatusDataSource to eliminate the boilerplate of implementing them for a custom type that has an existing implementation as one of its fields.
Once you have created your AppState
type aggregating the state for each API module, you can
initialize the state as normal, instantiating D
with a concrete implementation of a data
source and initializing hotshot_qs
as you normally would that data source.
However, this only works if you want the persistent storage for the availability and node
modules (managed by hotshot_qs
) to be independent of the persistent storage for other modules.
You may well want to synchronize the storage for all modules together, so that updates to the
entire application state can be done atomically. This is particularly relevant if one of your
application-specific modules updates its storage based on a stream of HotShot leaves. Since the
availability and node modules also update with each new leaf, you probably want all of these
modules to stay in sync. The data source implementations provided by this crate provide means by
which you can add additional data to the same persistent store and synchronize the entire store
together. Refer to the documentation for you specific data source for information on how to
achieve this.
Modules§
- api 🔒
- availability
- Queries for HotShot chain state.
- data_
source - Persistent storage and sources of data consumed by APIs.
- error 🔒
- explorer
- fetching
- Fetching missing data from remote providers.
- merklized_
state - Api for querying merklized state
- metrics
- node
- A node’s view of a HotShot chain
- resolvable 🔒
- status
- Queries for node-specific state and uncommitted data.
- task
- Async task utilities.
- types
- Common functionality provided by types used in this crate.
Macros§
- include_
migrations - Embed migrations from the given directory into the current binary for PostgreSQL or SQLite.
- instantiate_
data_ source_ tests
Structs§
- ApiState
- Read-only wrapper for API state which does not require locking.
- Error
Snafu - SNAFU context selector for the
QueryError::Error
variant - Leaf2
- This is the consensus-internal analogous concept to a block, and it contains the block proper,
as well as the hash of its parent
Leaf
. - Missing
Snafu - SNAFU context selector for the
QueryError::Missing
variant - NotFound
Snafu - SNAFU context selector for the
QueryError::NotFound
variant - Options
Enums§
Traits§
- Resolvable
- A reference to a
T
which can be resolved into a wholeT
.
Functions§
- run_
standalone_ service - Run an instance of the HotShot Query service with no customization.
Type Aliases§
- Header
- Metadata
- Payload
- Query
Result - Quorum
Certificate - Type alias for a
QuorumCertificate
, which is aSimpleCertificate
overQuorumData
- Signature
Key - Transaction
- Item within a
Payload
.