Crate hotshot_query_service

Source
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.
ErrorSnafu
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.
MissingSnafu
SNAFU context selector for the QueryError::Missing variant
NotFoundSnafu
SNAFU context selector for the QueryError::NotFound variant
Options

Enums§

Error
QueryError
VidCommon

Traits§

Resolvable
A reference to a T which can be resolved into a whole T.

Functions§

run_standalone_service
Run an instance of the HotShot Query service with no customization.

Type Aliases§

Header
Metadata
Payload
QueryResult
QuorumCertificate
Type alias for a QuorumCertificate, which is a SimpleCertificate over QuorumData
SignatureKey
Transaction
Item within a Payload.