diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc95487d0..ec739dd778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - Add `stackerdb_timeout_secs` to miner config for limiting duration of StackerDB HTTP requests. - When determining a global transaction replay set, the state evaluator now uses a longest-common-prefix algorithm to find a replay set in the case where a single replay set has less than 70% of signer weight. +- New endpoint /v3/tenures/blocks/ allowing retrieving the list of stacks blocks from a burn block ### Changed diff --git a/docs/rpc/components/examples/tenure-blocks.example.json b/docs/rpc/components/examples/tenure-blocks.example.json new file mode 100644 index 0000000000..73b3a5f384 --- /dev/null +++ b/docs/rpc/components/examples/tenure-blocks.example.json @@ -0,0 +1,77 @@ +{ + "burn_block_hash": "dff0aba3d65209225e365692ac46d26fa64ca9760bd7c244fc27e2022f7cb90d", + "burn_block_height": 51, + "consensus_hash": "90b360130732dd83a165a7fa34386ae547512300", + "stacks_blocks": [ + { + "block_hash": "a9f3d8bca7d766a2022528e27c52f39ba77192f0712ea5515cc9d06b97821a11", + "block_id": "67c795896b4c2940b3eb759871661a72e78458ba591dc63979d689d23695891f", + "header_type": "nakamoto", + "height": 116, + "parent_block_id": "fb7102c45bb2f07a6dd86b6672cb39133861fc7d38087e82cb04d4faccfcab82" + }, + { + "block_hash": "3e81505acaa3d386e15256921dd1193e479fd9d7f81d993b1c2b327b9bfb05cc", + "block_id": "fb7102c45bb2f07a6dd86b6672cb39133861fc7d38087e82cb04d4faccfcab82", + "header_type": "nakamoto", + "height": 115, + "parent_block_id": "5d51f815bc71508768b71258d8bc2adc68c129428101a418083f5b5ab30d525e" + }, + { + "block_hash": "c6f427503dcbe8bf187c7ce523c7a239328b2ab4ec87d6013895b2bd9ed23604", + "block_id": "5d51f815bc71508768b71258d8bc2adc68c129428101a418083f5b5ab30d525e", + "header_type": "nakamoto", + "height": 114, + "parent_block_id": "7d1c89f57a467f888fed171a5b3a2e51a7652f862189710df7db814ef84a29e2" + }, + { + "block_hash": "f0665d1dd419e949d68c4e85b7f79e078d6c36d929820b9cd9c794a60c0505b4", + "block_id": "7d1c89f57a467f888fed171a5b3a2e51a7652f862189710df7db814ef84a29e2", + "header_type": "nakamoto", + "height": 113, + "parent_block_id": "b65ab3b6d113285390c1d99c09a62d940f5b55c1cbc10bdc1b478c395fc435f9" + }, + { + "block_hash": "0486701e4eed06b74c3d93b82ffc0f3700b3cb2bea41aea1a30e00a5669ec0b7", + "block_id": "b65ab3b6d113285390c1d99c09a62d940f5b55c1cbc10bdc1b478c395fc435f9", + "header_type": "nakamoto", + "height": 112, + "parent_block_id": "c711415e6eedffcb6be16e33eb5c5cf62c3f51047f9528a51e21cbd83b25c185" + }, + { + "block_hash": "6724170340a2e507dbe7b21b691412e196c7c8900a597786aa991c1429ae6b58", + "block_id": "c711415e6eedffcb6be16e33eb5c5cf62c3f51047f9528a51e21cbd83b25c185", + "header_type": "nakamoto", + "height": 111, + "parent_block_id": "b11d631ef1b052942973cac88527a56f6e00c51fd57f9b7a710713f58ddc2393" + }, + { + "block_hash": "2114066e4446242e558847f552ca839f65ae769ab59000087677007e52b0dab7", + "block_id": "b11d631ef1b052942973cac88527a56f6e00c51fd57f9b7a710713f58ddc2393", + "header_type": "nakamoto", + "height": 110, + "parent_block_id": "2ef1973e95ba622479e59e87251d35c1eaaa9cb7f99f3dd3a8a79359624c38eb" + }, + { + "block_hash": "2345d34722cb07e908c5dab7f89ec0756f34d7fe34c86e40881c29c6aa202601", + "block_id": "2ef1973e95ba622479e59e87251d35c1eaaa9cb7f99f3dd3a8a79359624c38eb", + "header_type": "nakamoto", + "height": 109, + "parent_block_id": "9c1f844267819dc7349ff0aff6e6c6f60d8c41b531e304cd89df022ca4d147bf" + }, + { + "block_hash": "6b904297fdd045e2d18b93083f9d36592fa6915a53e8461fc3b4c59f6c91bc6d", + "block_id": "9c1f844267819dc7349ff0aff6e6c6f60d8c41b531e304cd89df022ca4d147bf", + "header_type": "nakamoto", + "height": 108, + "parent_block_id": "335596dd153bfc155863a2d439940816cd5095a3b36e2738fb89d715c28ac6c5" + }, + { + "block_hash": "cd748c67a9b7e685c52f388cc1d5e334b3dbe2509535caaf9f7ea7b6e5bbad77", + "block_id": "335596dd153bfc155863a2d439940816cd5095a3b36e2738fb89d715c28ac6c5", + "header_type": "nakamoto", + "height": 107, + "parent_block_id": "45cb658a80897117c2cfa6191a7623344ed1079117ad8bb30113019278bac028" + } + ] +} \ No newline at end of file diff --git a/docs/rpc/components/schemas/tenure-blocks.schema.yaml b/docs/rpc/components/schemas/tenure-blocks.schema.yaml new file mode 100644 index 0000000000..b79442bdb5 --- /dev/null +++ b/docs/rpc/components/schemas/tenure-blocks.schema.yaml @@ -0,0 +1,39 @@ +type: object +properties: + consensus_hash: + type: string + description: Consensus hash of the tenure + pattern: "^[0-9a-f]{40}$" + burn_block_height: + type: integer + description: Height of the Bitcoin block + format: uint64 + burn_block_hash: + type: string + description: Hash of the Bitcoin block + pattern: "^[0-9a-f]{64}$" + stacks_blocks: + type: array + items: + type: object + properties: + block_id: + type: string + description: Block ID (index block hash) + pattern: "^[0-9a-f]{64}$" + block_hash: + type: string + description: Hash of the block + pattern: "^[0-9a-f]{64}$" + header_type: + type: string + description: Type of block (currently "epoch2" or "nakamoto") + height: + type: integer + description: Height of the block + format: uint64 + parent_block_id: + type: string + description: Block ID (index block hash) of the parent + pattern: "^[0-9a-f]{64}$" + \ No newline at end of file diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml index 0fbd7a0042..f33d22f659 100644 --- a/docs/rpc/openapi.yaml +++ b/docs/rpc/openapi.yaml @@ -157,6 +157,8 @@ components: $ref: ./components/schemas/tenure-tip.schema.yaml GetStackerSet: $ref: ./components/schemas/get-stacker-set.schema.yaml + TenureBlocks: + $ref: ./components/schemas/tenure-blocks.schema.yaml paths: /v2/transactions: @@ -1057,6 +1059,40 @@ paths: type: string format: binary + /v3/tenures/blocks/{consensus_hash}: + get: + summary: Get the list of Stacks blocks in a tenure + tags: + - Blocks + security: [] + operationId: getTenureBlocks + description: | + Get the list of blocks in a tenure. The blocks will be + shown in order from highest to lowest. + parameters: + - name: consensus_hash + in: path + description: The hex-encoded consensus hash of the tenure to query (40 hexadecimal characters, without 0x prefix) + required: true + schema: + type: string + pattern: "^[0-9a-f]{40}$" + responses: + "200": + description: List of Stacks blocks in the tenure + content: + application/json: + schema: + $ref: "#/components/schemas/TenureBlocks" + example: + $ref: "./components/examples/tenure-blocks.example.json" + "400": + $ref: "#/components/responses/BadRequest" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalServerError" + /v3/sortitions: get: summary: Get latest sortition information diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index b54fef7eab..f6b40188c2 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -395,6 +395,13 @@ impl StacksHeaderInfo { pub fn is_nakamoto_block(&self) -> bool { matches!(self.anchored_header, StacksBlockHeaderTypes::Nakamoto(_)) } + + pub fn header_type_name(&self) -> &str { + match self.anchored_header { + StacksBlockHeaderTypes::Epoch2(_) => "epoch2", + StacksBlockHeaderTypes::Nakamoto(_) => "nakamoto", + } + } } impl FromRow for DBConfig { diff --git a/stackslib/src/net/api/gettenureblocks.rs b/stackslib/src/net/api/gettenureblocks.rs new file mode 100644 index 0000000000..fb73e902ad --- /dev/null +++ b/stackslib/src/net/api/gettenureblocks.rs @@ -0,0 +1,347 @@ +// Copyright (C) 2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity::types::chainstate::StacksBlockId; +use regex::{Captures, Regex}; +use serde_json; +use stacks_common::types::chainstate::{BlockHeaderHash, ConsensusHash}; +use stacks_common::types::net::PeerHost; + +use crate::chainstate::burn::db::DBConn; +use crate::chainstate::nakamoto::NakamotoChainState; +use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState}; +use crate::chainstate::stacks::Error as ChainError; +use crate::net::http::{ + parse_json, Error, HttpChunkGenerator, HttpNotFound, HttpRequest, HttpRequestContents, + HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload, + HttpResponsePreamble, HttpServerError, +}; +use crate::net::httpcore::{request, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse}; +use crate::net::{Error as NetError, StacksNodeState}; + +#[derive(Clone)] +pub struct RPCNakamotoTenureBlocksRequestHandler { + pub(crate) consensus_hash: Option, +} + +impl RPCNakamotoTenureBlocksRequestHandler { + pub fn new() -> Self { + Self { + consensus_hash: None, + } + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct RPCTenureBlock { + pub block_id: StacksBlockId, + pub header_type: String, + pub block_hash: BlockHeaderHash, + pub parent_block_id: StacksBlockId, + pub height: u64, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct RPCTenure { + pub consensus_hash: ConsensusHash, + pub burn_block_height: u64, + pub burn_block_hash: String, + pub stacks_blocks: Vec, +} + +pub struct RPCTenureStream { + /// connection to the headers DB + pub headers_conn: DBConn, + /// the tenure consensus hash + pub consensus_hash: ConsensusHash, + /// next block to process + pub next_block_id: StacksBlockId, + /// the first part of the json chunk to send (it is set to None after the chunk generation) + pub tenure_first_chunk: Option>, + /// do we need to send the last chunk? + pub last_chunk: bool, + /// is this the first block we are generating? + pub first_block: bool, +} + +impl RPCTenureStream { + /// Prepare for tenure streaming. + /// The tenure_first_chunk is created here and streamed at the first_next_block call + /// The HttpChunkGenerator trait implementation will take care of completing + /// the json stream (by clossing both the array and the object) + pub fn new( + chainstate: &StacksChainState, + block_id: StacksBlockId, + tenure: RPCTenure, + ) -> Result { + let headers_conn = chainstate.reopen_db()?; + let consensus_hash = tenure.consensus_hash; + let burn_block_height = tenure.burn_block_height; + let burn_block_hash = tenure.burn_block_hash; + let tenure_first_chunk = format!("{{\"consensus_hash\": \"{consensus_hash}\", \"burn_block_height\": {burn_block_height}, \"burn_block_hash\": \"{burn_block_hash}\", \"stacks_blocks\": ["); + Ok(RPCTenureStream { + headers_conn, + consensus_hash, + next_block_id: block_id, + tenure_first_chunk: Some(tenure_first_chunk.into_bytes()), + last_chunk: false, + first_block: true, + }) + } + + /// Stream the json block for the next block. + /// Stops on non-existent block or on a block in a different tenure + pub fn next_block(&mut self) -> Result, String> { + let block_header_opt = + NakamotoChainState::get_block_header(&self.headers_conn, &self.next_block_id) + .map_err(|e| format!("Chain error: {e}"))?; + + // stop if the block does not exist + let block_header = match block_header_opt { + Some(block_header) => block_header, + None => { + return Ok(vec![]); + } + }; + + // stop sending if the block is in a different tenure + if block_header.consensus_hash != self.consensus_hash { + return Ok(vec![]); + } + + let parent_block_id = match &block_header.anchored_header { + StacksBlockHeaderTypes::Nakamoto(nakamoto) => nakamoto.parent_block_id, + StacksBlockHeaderTypes::Epoch2(epoch2) => { + StacksBlockId::new(&self.consensus_hash, &epoch2.parent_block) + } + }; + + let block = RPCTenureBlock { + block_id: block_header.index_block_hash(), + header_type: block_header.header_type_name().into(), + block_hash: block_header.anchored_header.block_hash(), + parent_block_id, + height: block_header.stacks_block_height, + }; + + self.next_block_id = parent_block_id; + + let json = serde_json::to_string(&block) + .map_err(|e| format!("Failed to serialize block: {e:?}"))?; + Ok(json.into_bytes()) + } +} + +/// Stream implementation for a Nakamoto block +impl HttpChunkGenerator for RPCTenureStream { + #[cfg(test)] + fn hint_chunk_size(&self) -> usize { + // make this hurt + 32 + } + + #[cfg(not(test))] + fn hint_chunk_size(&self) -> usize { + 4096 + } + + fn generate_next_chunk(&mut self) -> Result, String> { + // last chunk? + if self.last_chunk { + return Ok(vec![]); + } + + // send the first chunk + if let Some(first_chunk) = self.tenure_first_chunk.take() { + self.tenure_first_chunk = None; + return Ok(first_chunk); + } + + // load up next block + let mut send_more = self.next_block().map_err(|e| { + let msg = format!("Failed to load next block in this tenure: {e:?}"); + warn!("{msg}"); + msg + })?; + + // end of blocks? + if send_more.is_empty() { + self.last_chunk = true; + return Ok(format!("]}}").into_bytes()); + } + + if !self.first_block { + send_more.insert(0, b','); + } + + self.first_block = false; + + Ok(send_more) + } +} + +/// Decode the HTTP request +impl HttpRequest for RPCNakamotoTenureBlocksRequestHandler { + fn verb(&self) -> &'static str { + "GET" + } + + fn path_regex(&self) -> Regex { + Regex::new(r#"^/v3/tenures/blocks/(?P[0-9a-f]{40})$"#).unwrap() + } + + fn metrics_identifier(&self) -> &str { + "/v3/tenures/blocks/:consensus_hash" + } + + /// Try to decode this request. + /// There's nothing to load here, so just make sure the request is well-formed. + fn try_parse_request( + &mut self, + preamble: &HttpRequestPreamble, + captures: &Captures, + query: Option<&str>, + _body: &[u8], + ) -> Result { + if preamble.get_content_length() != 0 { + return Err(Error::DecodeError( + "Invalid Http request: expected 0-length body".to_string(), + )); + } + let consensus_hash = request::get_consensus_hash(captures, "consensus_hash")?; + self.consensus_hash = Some(consensus_hash); + Ok(HttpRequestContents::new().query_string(query)) + } +} + +impl RPCRequestHandler for RPCNakamotoTenureBlocksRequestHandler { + /// Reset internal state + fn restart(&mut self) { + self.consensus_hash = None; + } + + /// Make the response + fn try_handle_request( + &mut self, + preamble: HttpRequestPreamble, + _contents: HttpRequestContents, + node: &mut StacksNodeState, + ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { + let consensus_hash = self + .consensus_hash + .take() + .ok_or(NetError::SendError("`consensus_hash` not set".into()))?; + + let stream_res = + node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| { + let header_info = + match NakamotoChainState::find_highest_known_block_header_in_tenure( + &chainstate, + sortdb, + &consensus_hash, + ) { + Ok(Some(header)) => header, + Ok(None) => { + let msg = format!("No blocks in tenure {consensus_hash}"); + debug!("{msg}"); + return Err(StacksHttpResponse::new_error( + &preamble, + &HttpNotFound::new(msg), + )); + } + Err(e) => { + let msg = format!( + "Failed to query tenure blocks by consensus '{consensus_hash}': {e:?}" + ); + error!("{msg}"); + return Err(StacksHttpResponse::new_error( + &preamble, + &HttpServerError::new(msg), + )); + } + }; + + let tenure = RPCTenure { + consensus_hash: header_info.consensus_hash, + burn_block_height: header_info.burn_header_height.into(), + burn_block_hash: header_info.burn_header_hash.to_hex(), + stacks_blocks: vec![], + }; + + match RPCTenureStream::new(chainstate, header_info.index_block_hash(), tenure) { + Ok(stream) => Ok(stream), + Err(e) => { + let msg = format!("Failed to create tenure stream: {e:?}"); + error!("{msg}"); + return Err(StacksHttpResponse::new_error( + &preamble, + &HttpServerError::new(msg), + )); + } + } + }); + + let stream = match stream_res { + Ok(stream) => stream, + Err(e) => { + let msg = format!("Failed to create tenure stream: {e:?}"); + error!("{msg}"); + return e.into(); + } + }; + + let preamble = HttpResponsePreamble::ok_json(&preamble); + let body = HttpResponseContents::from_stream(Box::new(stream)); + Ok((preamble, body)) + } +} + +/// Decode the HTTP response +impl HttpResponse for RPCNakamotoTenureBlocksRequestHandler { + fn try_parse_response( + &self, + preamble: &HttpResponsePreamble, + body: &[u8], + ) -> Result { + let tenure: RPCTenure = parse_json(preamble, body)?; + Ok(HttpResponsePayload::try_from_json(tenure)?) + } +} + +impl StacksHttpRequest { + /// Make a new getinfo request to this endpoint + pub fn new_get_tenure_blocks( + host: PeerHost, + consensus_hash: &ConsensusHash, + ) -> StacksHttpRequest { + StacksHttpRequest::new_for_peer( + host, + "GET".into(), + format!("/v3/tenures/blocks/{consensus_hash}"), + HttpRequestContents::new(), + ) + .expect("FATAL: failed to construct request from infallible data") + } +} + +impl StacksHttpResponse { + pub fn decode_tenure_blocks(self) -> Result { + let contents = self.get_http_payload_ok()?; + let response_json: serde_json::Value = contents.try_into()?; + let tenure: RPCTenure = serde_json::from_value(response_json) + .map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?; + Ok(tenure) + } +} diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs index 2f2f052eb6..af17424f60 100644 --- a/stackslib/src/net/api/mod.rs +++ b/stackslib/src/net/api/mod.rs @@ -49,6 +49,7 @@ pub mod getstackerdbmetadata; pub mod getstackers; pub mod getstxtransfercost; pub mod gettenure; +pub mod gettenureblocks; pub mod gettenureinfo; pub mod gettenuretip; pub mod gettransaction; @@ -119,6 +120,7 @@ impl StacksHttp { self.register_rpc_endpoint(gettenure::RPCNakamotoTenureRequestHandler::new()); self.register_rpc_endpoint(gettenureinfo::RPCNakamotoTenureInfoRequestHandler::new()); self.register_rpc_endpoint(gettenuretip::RPCNakamotoTenureTipRequestHandler::new()); + self.register_rpc_endpoint(gettenureblocks::RPCNakamotoTenureBlocksRequestHandler::new()); self.register_rpc_endpoint(get_tenures_fork_info::GetTenuresForkInfo::default()); self.register_rpc_endpoint( gettransaction_unconfirmed::RPCGetTransactionUnconfirmedRequestHandler::new(), diff --git a/stackslib/src/net/api/tests/gettenureblocks.rs b/stackslib/src/net/api/tests/gettenureblocks.rs new file mode 100644 index 0000000000..91cdfe4ea0 --- /dev/null +++ b/stackslib/src/net/api/tests/gettenureblocks.rs @@ -0,0 +1,157 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use stacks_common::types::chainstate::ConsensusHash; + +use crate::net::api::gettenureblocks; +use crate::net::api::tests::TestRPC; +use crate::net::connection::ConnectionOptions; +use crate::net::httpcore::{StacksHttp, StacksHttpRequest}; +use crate::net::test::TestEventObserver; +use crate::net::ProtocolFamily; + +#[test] +fn test_try_parse_request() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default()); + + let request = StacksHttpRequest::new_get_tenure_blocks(addr.into(), &ConsensusHash([0x01; 20])); + + let bytes = request.try_serialize().unwrap(); + + debug!("Request:\n{}\n", std::str::from_utf8(&bytes).unwrap()); + + let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap(); + + let mut handler = gettenureblocks::RPCNakamotoTenureBlocksRequestHandler::new(); + let mut parsed_request = http + .handle_try_parse_request( + &mut handler, + &parsed_preamble.expect_request(), + &bytes[offset..], + ) + .unwrap(); + assert_eq!(handler.consensus_hash, Some(ConsensusHash([0x01; 20]))); + + // parsed request consumes headers that would not be in a constructed request + parsed_request.clear_headers(); + let (preamble, contents) = parsed_request.destruct(); + + assert_eq!(&preamble, request.preamble()); +} + +#[test] +fn test_try_make_response() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + + let test_observer = TestEventObserver::new(); + let rpc_test = TestRPC::setup_nakamoto(function_name!(), &test_observer); + + let nakamoto_consensus_hash = rpc_test.consensus_hash.clone(); + + let mut requests = vec![]; + + // query existing, non-empty Nakamoto tenure + let request = + StacksHttpRequest::new_get_tenure_blocks(addr.clone().into(), &nakamoto_consensus_hash); + requests.push(request); + + let genesis_consensus_hash = test_observer + .get_blocks() + .first() + .unwrap() + .metadata + .consensus_hash; + + // query existing, non-empty Epoch2 tenure + let request = + StacksHttpRequest::new_get_tenure_blocks(addr.clone().into(), &genesis_consensus_hash); + requests.push(request); + + // query non-existant tenure + let request = + StacksHttpRequest::new_get_tenure_blocks(addr.clone().into(), &ConsensusHash([0x01; 20])); + requests.push(request); + + let mut responses = rpc_test.run(requests); + + // got the Nakamoto tip + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let resp = response.decode_tenure_blocks().unwrap(); + assert_eq!(resp.consensus_hash, nakamoto_consensus_hash); + let mut blocks_index = 0; + for block in test_observer.get_blocks().iter().rev() { + if block.metadata.consensus_hash != nakamoto_consensus_hash { + break; + } + + assert_eq!( + resp.stacks_blocks[blocks_index].block_id, + block.metadata.index_block_hash() + ); + + assert_eq!( + resp.stacks_blocks[blocks_index].parent_block_id.to_string(), + block.parent.to_hex() + ); + + assert_eq!(resp.stacks_blocks[blocks_index].header_type, "nakamoto"); + + blocks_index += 1; + } + + assert_eq!(blocks_index, resp.stacks_blocks.len()); + + // got Epoch2 (genesis) + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let resp = response.decode_tenure_blocks().unwrap(); + assert_eq!(resp.consensus_hash, genesis_consensus_hash); + + let blocks = test_observer.get_blocks(); + + let block = blocks.first().unwrap(); + + assert_eq!(resp.stacks_blocks.len(), 1); + + assert_eq!( + resp.stacks_blocks[0].block_id, + block.metadata.index_block_hash() + ); + + assert_eq!(resp.stacks_blocks[0].header_type, "epoch2"); + + // got a failure + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let (preamble, body) = response.destruct(); + assert_eq!(preamble.status_code, 404); +} diff --git a/stackslib/src/net/api/tests/mod.rs b/stackslib/src/net/api/tests/mod.rs index 8471bba8e9..4d7c3390fc 100644 --- a/stackslib/src/net/api/tests/mod.rs +++ b/stackslib/src/net/api/tests/mod.rs @@ -90,6 +90,7 @@ mod getstackerdbchunk; mod getstackerdbmetadata; mod getstxtransfercost; mod gettenure; +mod gettenureblocks; mod gettenureinfo; mod gettenuretip; mod gettransaction;