diff --git a/Cargo.lock b/Cargo.lock index 655edc479c..a4c621a104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5229,6 +5229,7 @@ dependencies = [ "refinery", "refinery-core", "reqwest 0.12.12", + "semver 1.0.24", "serde", "serde_json", "snafu 0.8.5", @@ -9877,6 +9878,7 @@ dependencies = [ "rand_chacha 0.3.1", "rand_distr", "reqwest 0.12.12", + "semver 1.0.24", "sequencer", "sequencer-utils", "serde", diff --git a/Cargo.toml b/Cargo.toml index d041a039cb..0ef8f0c76d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,6 +117,7 @@ libp2p-swarm-derive = { version = "0.35" } typenum = "1" cbor4ii = { version = "1.0", features = ["serde1"] } serde_bytes = { version = "0.11" } +semver = "1" num_cpus = "1" dashmap = "6" memoize = { version = "0.4", features = ["full"] } diff --git a/hotshot-query-service/Cargo.toml b/hotshot-query-service/Cargo.toml index da843a25bd..39048aa188 100644 --- a/hotshot-query-service/Cargo.toml +++ b/hotshot-query-service/Cargo.toml @@ -77,6 +77,7 @@ lazy_static = "1" prometheus = "0.13" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +semver = { workspace = true } snafu = "0.8" surf-disco = "0.9" tagged-base64 = "0.4" diff --git a/hotshot-query-service/src/availability.rs b/hotshot-query-service/src/availability.rs index 03cb36f744..e1382aefd7 100644 --- a/hotshot-query-service/src/availability.rs +++ b/hotshot-query-service/src/availability.rs @@ -29,7 +29,12 @@ use crate::{api::load_api, Payload, QueryError}; use derive_more::From; use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt}; -use hotshot_types::traits::node_implementation::NodeType; + +use hotshot_types::{ + data::{Leaf, Leaf2, QuorumProposal}, + simple_certificate::QuorumCertificate, + traits::node_implementation::NodeType, +}; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, Snafu}; use std::{fmt::Display, path::PathBuf, time::Duration}; @@ -161,9 +166,97 @@ impl Error { } } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(bound = "")] +pub struct Leaf1QueryData { + pub(crate) leaf: Leaf, + pub(crate) qc: QuorumCertificate, +} + +fn downgrade_leaf(leaf2: Leaf2) -> Leaf { + // TODO do we still need some check here? + // `drb_seed` no longer exists on `Leaf2` + // if leaf2.drb_seed != [0; 32] && leaf2.drb_result != [0; 32] { + // panic!("Downgrade of Leaf2 to Leaf will lose DRB information!"); + // } + let quorum_proposal = QuorumProposal { + block_header: leaf2.block_header().clone(), + view_number: leaf2.view_number(), + justify_qc: leaf2.justify_qc().to_qc(), + upgrade_certificate: leaf2.upgrade_certificate(), + proposal_certificate: None, + }; + let mut leaf = Leaf::from_quorum_proposal(&quorum_proposal); + if let Some(payload) = leaf2.block_payload() { + leaf.fill_block_payload_unchecked(payload); + } + leaf +} + +impl From> for Leaf1QueryData { + fn from(value: LeafQueryData) -> Self { + Self { + leaf: downgrade_leaf(value.leaf), + qc: value.qc.to_qc(), + } + } +} + +async fn get_leaf_handler( + req: tide_disco::RequestParams, + state: &State, + timeout: Duration, +) -> Result, Error> +where + State: 'static + Send + Sync + ReadState, + ::State: Send + Sync + AvailabilityDataSource, + Types: NodeType, + Payload: QueryablePayload, +{ + let id = match req.opt_integer_param("height")? { + Some(height) => LeafId::Number(height), + None => LeafId::Hash(req.blob_param("hash")?), + }; + let fetch = state.read(|state| state.get_leaf(id).boxed()).await; + fetch.with_timeout(timeout).await.context(FetchLeafSnafu { + resource: id.to_string(), + }) +} + +async fn get_leaf_range_handler( + req: tide_disco::RequestParams, + state: &State, + timeout: Duration, + small_object_range_limit: usize, +) -> Result>, Error> +where + State: 'static + Send + Sync + ReadState, + ::State: Send + Sync + AvailabilityDataSource, + Types: NodeType, + Payload: QueryablePayload, +{ + let from = req.integer_param::<_, usize>("from")?; + let until = req.integer_param("until")?; + enforce_range_limit(from, until, small_object_range_limit)?; + + let leaves = state + .read(|state| state.get_leaf_range(from..until).boxed()) + .await; + leaves + .enumerate() + .then(|(index, fetch)| async move { + fetch.with_timeout(timeout).await.context(FetchLeafSnafu { + resource: (index + from).to_string(), + }) + }) + .try_collect::>() + .await +} + pub fn define_api( options: &Options, _: Ver, + api_ver: semver::Version, ) -> Result, ApiError> where State: 'static + Send + Sync + ReadState, @@ -179,310 +272,331 @@ where let small_object_range_limit = options.small_object_range_limit; let large_object_range_limit = options.large_object_range_limit; - api.with_version("0.0.1".parse().unwrap()) - .at("get_leaf", move |req, state| { - async move { - let id = match req.opt_integer_param("height")? { - Some(height) => LeafId::Number(height), - None => LeafId::Hash(req.blob_param("hash")?), - }; - let fetch = state.read(|state| state.get_leaf(id).boxed()).await; - fetch.with_timeout(timeout).await.context(FetchLeafSnafu { - resource: id.to_string(), - }) - } - .boxed() - })? - .at("get_leaf_range", move |req, state| { - async move { - let from = req.integer_param::<_, usize>("from")?; - let until = req.integer_param("until")?; - enforce_range_limit(from, until, small_object_range_limit)?; - - let leaves = state - .read(|state| state.get_leaf_range(from..until).boxed()) - .await; - leaves - .enumerate() - .then(|(index, fetch)| async move { - fetch.with_timeout(timeout).await.context(FetchLeafSnafu { - resource: (index + from).to_string(), - }) + api.with_version(api_ver.clone()); + + // `LeafQueryData` now contains `Leaf2` and `QC2``, which is a breaking change. + // On node startup, all leaves are migrated to `Leaf2`. + // + // To maintain compatibility with nodes running an older version + // (which expect `LeafQueryData` with `Leaf1` and `QC1`), + // we downgrade `Leaf2` to `Leaf1` and `QC2` to `QC1` if the API version is V0. + // Otherwise, we return the new types. + if api_ver.major == 0 { + api.at("get_leaf", move |req, state| { + get_leaf_handler(req, state, timeout) + .map(|res| res.map(Leaf1QueryData::from)) + .boxed() + })?; + + api.at("get_leaf_range", move |req, state| { + get_leaf_range_handler(req, state, timeout, small_object_range_limit) + .map(|res| { + res.map(|r| { + r.into_iter() + .map(Into::into) + .collect::>>() }) - .try_collect::>() - .await - } - .boxed() - })? - .stream("stream_leaves", move |req, state| { + }) + .boxed() + })?; + + api.stream("stream_leaves", move |req, state| { async move { let height = req.integer_param("height")?; state .read(|state| { - async move { Ok(state.subscribe_leaves(height).await.map(Ok)) }.boxed() + async move { + Ok(state + .subscribe_leaves(height) + .await + .map(|leaf| Ok(Leaf1QueryData::from(leaf)))) + } + .boxed() }) .await } .try_flatten_stream() .boxed() - })? - .at("get_header", move |req, state| { - async move { - let id = if let Some(height) = req.opt_integer_param("height")? { - BlockId::Number(height) - } else if let Some(hash) = req.opt_blob_param("hash")? { - BlockId::Hash(hash) - } else { - BlockId::PayloadHash(req.blob_param("payload-hash")?) - }; - let fetch = state.read(|state| state.get_header(id).boxed()).await; - fetch.with_timeout(timeout).await.context(FetchHeaderSnafu { - resource: id.to_string(), - }) - } - .boxed() - })? - .at("get_header_range", move |req, state| { - async move { - let from = req.integer_param::<_, usize>("from")?; - let until = req.integer_param::<_, usize>("until")?; - enforce_range_limit(from, until, large_object_range_limit)?; - - let headers = state - .read(|state| state.get_header_range(from..until).boxed()) - .await; - headers - .enumerate() - .then(|(index, fetch)| async move { - fetch.with_timeout(timeout).await.context(FetchHeaderSnafu { - resource: (index + from).to_string(), - }) - }) - .try_collect::>() - .await - } - .boxed() - })? - .stream("stream_headers", move |req, state| { + })?; + } else { + api.at("get_leaf", move |req, state| { + get_leaf_handler(req, state, timeout).boxed() + })?; + + api.at("get_leaf_range", move |req, state| { + get_leaf_range_handler(req, state, timeout, small_object_range_limit).boxed() + })?; + + api.stream("stream_leaves", move |req, state| { async move { let height = req.integer_param("height")?; state .read(|state| { - async move { Ok(state.subscribe_headers(height).await.map(Ok)) }.boxed() + async move { Ok(state.subscribe_leaves(height).await.map(Ok)) }.boxed() }) .await } .try_flatten_stream() .boxed() - })? - .at("get_block", move |req, state| { - async move { - let id = if let Some(height) = req.opt_integer_param("height")? { - BlockId::Number(height) - } else if let Some(hash) = req.opt_blob_param("hash")? { - BlockId::Hash(hash) - } else { - BlockId::PayloadHash(req.blob_param("payload-hash")?) - }; - let fetch = state.read(|state| state.get_block(id).boxed()).await; - fetch.with_timeout(timeout).await.context(FetchBlockSnafu { - resource: id.to_string(), + })?; + } + api.at("get_header", move |req, state| { + async move { + let id = if let Some(height) = req.opt_integer_param("height")? { + BlockId::Number(height) + } else if let Some(hash) = req.opt_blob_param("hash")? { + BlockId::Hash(hash) + } else { + BlockId::PayloadHash(req.blob_param("payload-hash")?) + }; + let fetch = state.read(|state| state.get_header(id).boxed()).await; + fetch.with_timeout(timeout).await.context(FetchHeaderSnafu { + resource: id.to_string(), + }) + } + .boxed() + })? + .at("get_header_range", move |req, state| { + async move { + let from = req.integer_param::<_, usize>("from")?; + let until = req.integer_param::<_, usize>("until")?; + enforce_range_limit(from, until, large_object_range_limit)?; + + let headers = state + .read(|state| state.get_header_range(from..until).boxed()) + .await; + headers + .enumerate() + .then(|(index, fetch)| async move { + fetch.with_timeout(timeout).await.context(FetchHeaderSnafu { + resource: (index + from).to_string(), + }) }) - } - .boxed() - })? - .at("get_block_range", move |req, state| { - async move { - let from = req.integer_param::<_, usize>("from")?; - let until = req.integer_param("until")?; - enforce_range_limit(from, until, large_object_range_limit)?; - - let blocks = state - .read(|state| state.get_block_range(from..until).boxed()) - .await; - blocks - .enumerate() - .then(|(index, fetch)| async move { - fetch.with_timeout(timeout).await.context(FetchBlockSnafu { - resource: (index + from).to_string(), - }) + .try_collect::>() + .await + } + .boxed() + })? + .stream("stream_headers", move |req, state| { + async move { + let height = req.integer_param("height")?; + state + .read(|state| { + async move { Ok(state.subscribe_headers(height).await.map(Ok)) }.boxed() + }) + .await + } + .try_flatten_stream() + .boxed() + })? + .at("get_block", move |req, state| { + async move { + let id = if let Some(height) = req.opt_integer_param("height")? { + BlockId::Number(height) + } else if let Some(hash) = req.opt_blob_param("hash")? { + BlockId::Hash(hash) + } else { + BlockId::PayloadHash(req.blob_param("payload-hash")?) + }; + let fetch = state.read(|state| state.get_block(id).boxed()).await; + fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + resource: id.to_string(), + }) + } + .boxed() + })? + .at("get_block_range", move |req, state| { + async move { + let from = req.integer_param::<_, usize>("from")?; + let until = req.integer_param("until")?; + enforce_range_limit(from, until, large_object_range_limit)?; + + let blocks = state + .read(|state| state.get_block_range(from..until).boxed()) + .await; + blocks + .enumerate() + .then(|(index, fetch)| async move { + fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + resource: (index + from).to_string(), }) - .try_collect::>() - .await - } - .boxed() - })? - .stream("stream_blocks", move |req, state| { - async move { - let height = req.integer_param("height")?; - state - .read(|state| { - async move { Ok(state.subscribe_blocks(height).await.map(Ok)) }.boxed() + }) + .try_collect::>() + .await + } + .boxed() + })? + .stream("stream_blocks", move |req, state| { + async move { + let height = req.integer_param("height")?; + state + .read(|state| { + async move { Ok(state.subscribe_blocks(height).await.map(Ok)) }.boxed() + }) + .await + } + .try_flatten_stream() + .boxed() + })? + .at("get_payload", move |req, state| { + async move { + let id = if let Some(height) = req.opt_integer_param("height")? { + BlockId::Number(height) + } else if let Some(hash) = req.opt_blob_param("hash")? { + BlockId::PayloadHash(hash) + } else { + BlockId::Hash(req.blob_param("block-hash")?) + }; + let fetch = state.read(|state| state.get_payload(id).boxed()).await; + fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + resource: id.to_string(), + }) + } + .boxed() + })? + .at("get_payload_range", move |req, state| { + async move { + let from = req.integer_param::<_, usize>("from")?; + let until = req.integer_param("until")?; + enforce_range_limit(from, until, large_object_range_limit)?; + + let payloads = state + .read(|state| state.get_payload_range(from..until).boxed()) + .await; + payloads + .enumerate() + .then(|(index, fetch)| async move { + fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + resource: (index + from).to_string(), }) - .await - } - .try_flatten_stream() - .boxed() - })? - .at("get_payload", move |req, state| { - async move { - let id = if let Some(height) = req.opt_integer_param("height")? { - BlockId::Number(height) - } else if let Some(hash) = req.opt_blob_param("hash")? { - BlockId::PayloadHash(hash) - } else { - BlockId::Hash(req.blob_param("block-hash")?) - }; - let fetch = state.read(|state| state.get_payload(id).boxed()).await; - fetch.with_timeout(timeout).await.context(FetchBlockSnafu { - resource: id.to_string(), }) - } - .boxed() - })? - .at("get_payload_range", move |req, state| { - async move { - let from = req.integer_param::<_, usize>("from")?; - let until = req.integer_param("until")?; - enforce_range_limit(from, until, large_object_range_limit)?; - - let payloads = state - .read(|state| state.get_payload_range(from..until).boxed()) - .await; - payloads - .enumerate() - .then(|(index, fetch)| async move { - fetch.with_timeout(timeout).await.context(FetchBlockSnafu { - resource: (index + from).to_string(), + .try_collect::>() + .await + } + .boxed() + })? + .stream("stream_payloads", move |req, state| { + async move { + let height = req.integer_param("height")?; + state + .read(|state| { + async move { Ok(state.subscribe_payloads(height).await.map(Ok)) }.boxed() + }) + .await + } + .try_flatten_stream() + .boxed() + })? + .at("get_vid_common", move |req, state| { + async move { + let id = if let Some(height) = req.opt_integer_param("height")? { + BlockId::Number(height) + } else if let Some(hash) = req.opt_blob_param("hash")? { + BlockId::Hash(hash) + } else { + BlockId::PayloadHash(req.blob_param("payload-hash")?) + }; + let fetch = state.read(|state| state.get_vid_common(id).boxed()).await; + fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + resource: id.to_string(), + }) + } + .boxed() + })? + .stream("stream_vid_common", move |req, state| { + async move { + let height = req.integer_param("height")?; + state + .read(|state| { + async move { Ok(state.subscribe_vid_common(height).await.map(Ok)) }.boxed() + }) + .await + } + .try_flatten_stream() + .boxed() + })? + .at("get_transaction", move |req, state| { + async move { + match req.opt_blob_param("hash")? { + Some(hash) => { + let fetch = state + .read(|state| state.get_transaction(hash).boxed()) + .await; + fetch + .with_timeout(timeout) + .await + .context(FetchTransactionSnafu { + resource: hash.to_string(), }) - }) - .try_collect::>() - .await - } - .boxed() - })? - .stream("stream_payloads", move |req, state| { - async move { - let height = req.integer_param("height")?; - state - .read(|state| { - async move { Ok(state.subscribe_payloads(height).await.map(Ok)) }.boxed() - }) - .await + } + None => { + let height: u64 = req.integer_param("height")?; + let fetch = state + .read(|state| state.get_block(height as usize).boxed()) + .await; + let block = fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + resource: height.to_string(), + })?; + let i: u64 = req.integer_param("index")?; + let index = block + .payload() + .nth(block.metadata(), i as usize) + .context(InvalidTransactionIndexSnafu { height, index: i })?; + TransactionQueryData::new(&block, index, i) + .context(InvalidTransactionIndexSnafu { height, index: i }) + } } - .try_flatten_stream() - .boxed() - })? - .at("get_vid_common", move |req, state| { - async move { - let id = if let Some(height) = req.opt_integer_param("height")? { - BlockId::Number(height) - } else if let Some(hash) = req.opt_blob_param("hash")? { - BlockId::Hash(hash) - } else { - BlockId::PayloadHash(req.blob_param("payload-hash")?) - }; - let fetch = state.read(|state| state.get_vid_common(id).boxed()).await; - fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + } + .boxed() + })? + .at("get_block_summary", move |req, state| { + async move { + let id: usize = req.integer_param("height")?; + + let fetch = state.read(|state| state.get_block(id).boxed()).await; + fetch + .with_timeout(timeout) + .await + .context(FetchBlockSnafu { resource: id.to_string(), }) - } - .boxed() - })? - .stream("stream_vid_common", move |req, state| { - async move { - let height = req.integer_param("height")?; - state - .read(|state| { - async move { Ok(state.subscribe_vid_common(height).await.map(Ok)) }.boxed() - }) - .await - } - .try_flatten_stream() - .boxed() - })? - .at("get_transaction", move |req, state| { - async move { - match req.opt_blob_param("hash")? { - Some(hash) => { - let fetch = state - .read(|state| state.get_transaction(hash).boxed()) - .await; - fetch - .with_timeout(timeout) - .await - .context(FetchTransactionSnafu { - resource: hash.to_string(), - }) - } - None => { - let height: u64 = req.integer_param("height")?; - let fetch = state - .read(|state| state.get_block(height as usize).boxed()) - .await; - let block = fetch.with_timeout(timeout).await.context(FetchBlockSnafu { - resource: height.to_string(), - })?; - let i: u64 = req.integer_param("index")?; - let index = block - .payload() - .nth(block.metadata(), i as usize) - .context(InvalidTransactionIndexSnafu { height, index: i })?; - TransactionQueryData::new(&block, index, i) - .context(InvalidTransactionIndexSnafu { height, index: i }) - } - } - } - .boxed() - })? - .at("get_block_summary", move |req, state| { - async move { - let id: usize = req.integer_param("height")?; - - let fetch = state.read(|state| state.get_block(id).boxed()).await; - fetch - .with_timeout(timeout) - .await - .context(FetchBlockSnafu { - resource: id.to_string(), - }) - .map(BlockSummaryQueryData::from) - } - .boxed() - })? - .at("get_block_summary_range", move |req, state| { - async move { - let from: usize = req.integer_param("from")?; - let until: usize = req.integer_param("until")?; - enforce_range_limit(from, until, large_object_range_limit)?; - - let blocks = state - .read(|state| state.get_block_range(from..until).boxed()) - .await; - let result: Vec> = blocks - .enumerate() - .then(|(index, fetch)| async move { - fetch.with_timeout(timeout).await.context(FetchBlockSnafu { - resource: (index + from).to_string(), - }) + .map(BlockSummaryQueryData::from) + } + .boxed() + })? + .at("get_block_summary_range", move |req, state| { + async move { + let from: usize = req.integer_param("from")?; + let until: usize = req.integer_param("until")?; + enforce_range_limit(from, until, large_object_range_limit)?; + + let blocks = state + .read(|state| state.get_block_range(from..until).boxed()) + .await; + let result: Vec> = blocks + .enumerate() + .then(|(index, fetch)| async move { + fetch.with_timeout(timeout).await.context(FetchBlockSnafu { + resource: (index + from).to_string(), }) - .map(|result| result.map(BlockSummaryQueryData::from)) - .try_collect() - .await?; - - Ok(result) - } - .boxed() - })? - .at("get_limits", move |_req, _state| { - async move { - Ok(Limits { - small_object_range_limit, - large_object_range_limit, }) - } - .boxed() - })?; + .map(|result| result.map(BlockSummaryQueryData::from)) + .try_collect() + .await?; + + Ok(result) + } + .boxed() + })? + .at("get_limits", move |_req, _state| { + async move { + Ok(Limits { + small_object_range_limit, + large_object_range_limit, + }) + } + .boxed() + })?; Ok(api) } @@ -790,7 +904,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -926,6 +1045,7 @@ mod test { ..Default::default() }, MockBase::instance(), + "0.0.1".parse().unwrap(), ) .unwrap(); api.get("get_ext", |_, state| { @@ -996,6 +1116,7 @@ mod test { ..Default::default() }, MockBase::instance(), + "0.0.1".parse().unwrap(), ) .unwrap(), ) @@ -1080,7 +1201,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1117,7 +1243,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( diff --git a/hotshot-query-service/src/explorer.rs b/hotshot-query-service/src/explorer.rs index 03ca26323c..ef259a83e5 100644 --- a/hotshot-query-service/src/explorer.rs +++ b/hotshot-query-service/src/explorer.rs @@ -878,6 +878,7 @@ mod test { ..Default::default() }, MockBase::instance(), + "0.0.1".parse().unwrap(), ) .unwrap(), ) diff --git a/hotshot-query-service/src/fetching/provider/any.rs b/hotshot-query-service/src/fetching/provider/any.rs index f2f4c3536a..37d5b854db 100644 --- a/hotshot-query-service/src/fetching/provider/any.rs +++ b/hotshot-query-service/src/fetching/provider/any.rs @@ -234,7 +234,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); let _server = BackgroundTask::spawn( diff --git a/hotshot-query-service/src/fetching/provider/query_service.rs b/hotshot-query-service/src/fetching/provider/query_service.rs index 0fad9606c8..7e752c8222 100644 --- a/hotshot-query-service/src/fetching/provider/query_service.rs +++ b/hotshot-query-service/src/fetching/provider/query_service.rs @@ -249,7 +249,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -472,7 +477,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -530,7 +540,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -592,7 +607,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -651,7 +671,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -707,7 +732,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -778,7 +808,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -923,7 +958,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1091,7 +1131,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1190,7 +1235,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1282,7 +1332,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1346,7 +1401,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1404,7 +1464,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1480,7 +1545,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1570,7 +1640,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1637,7 +1712,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( @@ -1709,7 +1789,12 @@ mod test { let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", - define_api(&Default::default(), MockBase::instance()).unwrap(), + define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap(); network.spawn( diff --git a/hotshot-query-service/src/lib.rs b/hotshot-query-service/src/lib.rs index b215d8cad8..8c0be66106 100644 --- a/hotshot-query-service/src/lib.rs +++ b/hotshot-query-service/src/lib.rs @@ -541,8 +541,12 @@ where ApiVer: StaticVersionType + 'static, { // Create API modules. - let availability_api = - availability::define_api(&options.availability, bind_version).map_err(Error::internal)?; + let availability_api = availability::define_api( + &options.availability, + bind_version, + "0.0.1".parse().unwrap(), + ) + .map_err(Error::internal)?; let node_api = node::define_api(&options.node, bind_version).map_err(Error::internal)?; let status_api = status::define_api(&options.status, bind_version).map_err(Error::internal)?; @@ -860,7 +864,12 @@ mod test { let mut app = App::<_, Error>::with_state(RwLock::new(state)); app.register_module( "availability", - availability::define_api(&Default::default(), MockBase::instance()).unwrap(), + availability::define_api( + &Default::default(), + MockBase::instance(), + "0.0.1".parse().unwrap(), + ) + .unwrap(), ) .unwrap() .register_module( diff --git a/sequencer-sqlite/Cargo.lock b/sequencer-sqlite/Cargo.lock index acfa39f3b5..1a688e4ec9 100644 --- a/sequencer-sqlite/Cargo.lock +++ b/sequencer-sqlite/Cargo.lock @@ -4896,6 +4896,7 @@ dependencies = [ "prometheus", "refinery", "refinery-core", + "semver 1.0.24", "serde", "serde_json", "snafu 0.8.5", @@ -9262,6 +9263,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_distr", + "semver 1.0.24", "sequencer-utils", "serde", "serde_json", diff --git a/sequencer/Cargo.toml b/sequencer/Cargo.toml index 69564f90d5..777e8e785e 100644 --- a/sequencer/Cargo.toml +++ b/sequencer/Cargo.toml @@ -96,6 +96,7 @@ priority-queue = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } rand_distr = { workspace = true } +semver = { workspace = true } sequencer-utils = { path = "../utils" } serde = { workspace = true } serde_json = { workspace = true } diff --git a/sequencer/src/api/endpoints.rs b/sequencer/src/api/endpoints.rs index c33df0196a..ed661026e8 100644 --- a/sequencer/src/api/endpoints.rs +++ b/sequencer/src/api/endpoints.rs @@ -90,6 +90,7 @@ type AvailabilityApi = Api, availabil // Snafu has been replaced by `this_error` everywhere. // However, the query service still uses snafu pub(super) fn availability( + api_ver: semver::Version, ) -> Result> where N: ConnectedNetwork, @@ -104,6 +105,7 @@ where let mut api = availability::define_api::, SeqTypes, _>( &options, SequencerApiVersion::instance(), + api_ver, )?; api.get("getnamespaceproof", move |req, state| { diff --git a/sequencer/src/api/options.rs b/sequencer/src/api/options.rs index f2fa44552c..c1de9236f2 100644 --- a/sequencer/src/api/options.rs +++ b/sequencer/src/api/options.rs @@ -200,7 +200,25 @@ impl Options { app.register_module("status", status_api)?; // Initialize availability and node APIs (these both use the same data source). - app.register_module("availability", endpoints::availability()?)?; + + // Note: We initialize two versions of the availability module: `availability/v0` and `availability/v1`. + // - `availability/v0/leaf/0` returns the old `Leaf1` type for backward compatibility. + // - `availability/v1/leaf/0` returns the new `Leaf2` type + + // initialize the availability module for API version V0. + // This ensures compatibility for nodes that expect `Leaf1` for leaf endpoints + app.register_module( + "availability", + endpoints::availability("0.0.1".parse().unwrap())?, + )?; + + // initialize the availability module for API version V1. + // This enables support for the new `Leaf2` type + app.register_module( + "availability", + endpoints::availability("1.0.0".parse().unwrap())?, + )?; + app.register_module("node", endpoints::node()?)?; // Initialize submit API