hotshot_query_service/
merklized_state.rs

1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the HotShot Query Service library.
3//
4// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
5// General Public License as published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
8// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// General Public License for more details.
10// You should have received a copy of the GNU General Public License along with this program. If not,
11// see <https://www.gnu.org/licenses/>.
12
13//! Api for querying merklized state
14//!
15//! The state API provides an interface for serving queries against arbitrarily old snapshots of the state.
16//! This allows a full Merkle tree to be reconstructed from storage.
17//! If any parent state is missing then the partial snapshot can not be queried.
18use std::{
19    fmt::{Debug, Display},
20    path::PathBuf,
21};
22
23use derive_more::From;
24use futures::FutureExt;
25use hotshot_types::traits::node_implementation::NodeType;
26use serde::{Deserialize, Serialize};
27use snafu::{ResultExt, Snafu};
28use tagged_base64::TaggedBase64;
29use tide_disco::{api::ApiError, method::ReadState, Api, RequestError, StatusCode};
30use vbs::version::StaticVersionType;
31
32use crate::{api::load_api, QueryError};
33
34pub(crate) mod data_source;
35pub use data_source::*;
36
37#[derive(Default)]
38pub struct Options {
39    pub api_path: Option<PathBuf>,
40
41    /// Additional API specification files to merge with `merklized-state-api-path`.
42    ///
43    /// These optional files may contain route definitions for application-specific routes that have
44    /// been added as extensions to the basic status API.
45    pub extensions: Vec<toml::Value>,
46}
47
48#[derive(Clone, Debug, From, Snafu, Deserialize, Serialize)]
49pub enum Error {
50    Request {
51        source: RequestError,
52    },
53    #[snafu(display("{source}"))]
54    Query {
55        source: QueryError,
56    },
57    #[snafu(display("error {status}: {message}"))]
58    Custom {
59        message: String,
60        status: StatusCode,
61    },
62}
63
64impl Error {
65    pub fn status(&self) -> StatusCode {
66        match self {
67            Self::Request { .. } => StatusCode::BAD_REQUEST,
68            Self::Query { source, .. } => source.status(),
69            Self::Custom { status, .. } => *status,
70        }
71    }
72}
73
74pub fn define_api<
75    State,
76    Types: NodeType,
77    M: MerklizedState<Types, ARITY>,
78    Ver: StaticVersionType + 'static,
79    const ARITY: usize,
80>(
81    options: &Options,
82    api_ver: semver::Version,
83) -> Result<Api<State, Error, Ver>, ApiError>
84where
85    State: 'static + Send + Sync + ReadState,
86    <State as ReadState>::State:
87        MerklizedStateDataSource<Types, M, ARITY> + MerklizedStateHeightPersistence + Send + Sync,
88    for<'a> <M::Commit as TryFrom<&'a TaggedBase64>>::Error: Display,
89{
90    let mut api = load_api::<State, Error, Ver>(
91        options.api_path.as_ref(),
92        include_str!("../api/state.toml"),
93        options.extensions.clone(),
94    )?;
95
96    api.with_version(api_ver)
97        .get("get_path", move |req, state| {
98            async move {
99                // Determine the snapshot type based on request parameters, either index or commit
100                let snapshot = if let Some(height) = req.opt_integer_param("height")? {
101                    Snapshot::Index(height)
102                } else {
103                    Snapshot::Commit(req.blob_param("commit")?)
104                };
105
106                let key = req.string_param("key")?;
107                let key = key.parse::<M::Key>().map_err(|_| Error::Custom {
108                    message: "failed to parse Key param".to_string(),
109                    status: StatusCode::INTERNAL_SERVER_ERROR,
110                })?;
111
112                state.get_path(snapshot, key).await.context(QuerySnafu)
113            }
114            .boxed()
115        })?
116        .get("get_height", move |_, state| {
117            async move { state.get_last_state_height().await.context(QuerySnafu) }.boxed()
118        })?;
119
120    Ok(api)
121}