Skip to content

Commit 0177bbf

Browse files
committed
Merge branch 'feat/sip-029' of https://github.com/stacks-network/stacks-blockchain into feat/sip-029
2 parents 79ea227 + 28d0224 commit 0177bbf

File tree

11 files changed

+612
-0
lines changed

11 files changed

+612
-0
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ jobs:
142142
- tests::nakamoto_integrations::signer_chainstate
143143
- tests::nakamoto_integrations::sip029_coinbase_change
144144
- tests::nakamoto_integrations::clarity_cost_spend_down
145+
- tests::nakamoto_integrations::v3_blockbyheight_api_endpoint
145146
# TODO: enable these once v1 signer is supported by a new nakamoto epoch
146147
# - tests::signer::v1::dkg
147148
# - tests::signer::v1::sign_request_rejected

.github/workflows/p2p-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
- net::tests::convergence::test_walk_star_15_pingback
4343
- net::tests::convergence::test_walk_star_15_org_biased
4444
- net::tests::convergence::test_walk_inbound_line_15
45+
- net::api::tests::postblock_proposal::test_try_make_response
4546
steps:
4647
## Setup test environment
4748
- name: Setup Test Environment

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1313
- Add index to `metadata_table` in Clarity DB on `blockhash`
1414
- Add `block_commit_delay_ms` to the config file to control the time to wait after seeing a new burn block, before submitting a block commit, to allow time for the first Nakamoto block of the new tenure to be mined, allowing this miner to avoid the need to RBF the block commit.
1515
- Add `tenure_cost_limit_per_block_percentage` to the miner config file to control the percentage remaining tenure cost limit to consume per nakamoto block.
16+
- Add `/v3/blocks/height/:block_height` rpc endpoint
1617

1718
## [3.0.0.0.1]
1819

docs/rpc-endpoints.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,17 @@ data.
503503

504504
This will return 404 if the block does not exist.
505505

506+
### GET /v3/blocks/height/[Block Height]
507+
508+
Fetch a Nakamoto block given its block height. This returns the raw block
509+
data.
510+
511+
This will return 404 if the block does not exist.
512+
513+
This endpoint also accepts a querystring parameter `?tip=` which when supplied
514+
will return the block relative to the specified tip allowing the querying of
515+
sibling blocks (same height, different tip) too.
516+
506517
### GET /v3/tenures/[Block ID]
507518

508519
Fetch a Nakamoto block and all of its ancestors in the same tenure, given its

docs/rpc/openapi.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,40 @@ paths:
627627
content:
628628
application/text-plain: {}
629629

630+
/v3/blocks/height/{block_height}:
631+
get:
632+
summary: Fetch a Nakamoto block by its height and optional tip
633+
tags:
634+
- Blocks
635+
operationId: get_block_v3_by_height
636+
description:
637+
Fetch a Nakamoto block by its height and optional tip.
638+
parameters:
639+
- name: block_height
640+
in: path
641+
description: The block's height
642+
required: true
643+
schema:
644+
type: integer
645+
- name: tip
646+
in: query
647+
schema:
648+
type: string
649+
description: The Stacks chain tip to query from. If tip == latest or empty, the query will be run
650+
from the latest known tip.
651+
responses:
652+
"200":
653+
description: The raw SIP-003-encoded block will be returned.
654+
content:
655+
application/octet-stream:
656+
schema:
657+
type: string
658+
format: binary
659+
"404":
660+
description: The block could not be found
661+
content:
662+
application/text-plain: {}
663+
630664
/v3/tenures/info:
631665
get:
632666
summary: Fetch metadata about the ongoing Nakamoto tenure
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2+
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
use std::io::{Read, Seek, SeekFrom, Write};
18+
use std::{fs, io};
19+
20+
use regex::{Captures, Regex};
21+
use rusqlite::Connection;
22+
use serde::de::Error as de_Error;
23+
use stacks_common::codec::{StacksMessageCodec, MAX_MESSAGE_LEN};
24+
use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId};
25+
use stacks_common::types::net::PeerHost;
26+
use {serde, serde_json};
27+
28+
use crate::chainstate::nakamoto::{
29+
NakamotoBlock, NakamotoChainState, NakamotoStagingBlocksConn, StacksDBIndexed,
30+
};
31+
use crate::chainstate::stacks::db::StacksChainState;
32+
use crate::chainstate::stacks::Error as ChainError;
33+
use crate::net::api::getblock_v3::{NakamotoBlockStream, RPCNakamotoBlockRequestHandler};
34+
use crate::net::http::{
35+
parse_bytes, Error, HttpBadRequest, HttpChunkGenerator, HttpContentType, HttpNotFound,
36+
HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse, HttpResponseContents,
37+
HttpResponsePayload, HttpResponsePreamble, HttpServerError, HttpVersion,
38+
};
39+
use crate::net::httpcore::{
40+
HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp, StacksHttpRequest,
41+
StacksHttpResponse,
42+
};
43+
use crate::net::{Error as NetError, StacksNodeState, TipRequest, MAX_HEADERS};
44+
use crate::util_lib::db::{DBConn, Error as DBError};
45+
46+
#[derive(Clone)]
47+
pub struct RPCNakamotoBlockByHeightRequestHandler {
48+
pub block_height: Option<u64>,
49+
}
50+
51+
impl RPCNakamotoBlockByHeightRequestHandler {
52+
pub fn new() -> Self {
53+
Self { block_height: None }
54+
}
55+
}
56+
57+
/// Decode the HTTP request
58+
impl HttpRequest for RPCNakamotoBlockByHeightRequestHandler {
59+
fn verb(&self) -> &'static str {
60+
"GET"
61+
}
62+
63+
fn path_regex(&self) -> Regex {
64+
Regex::new(r#"^/v3/blocks/height/(?P<block_height>[0-9]{1,20})$"#).unwrap()
65+
}
66+
67+
fn metrics_identifier(&self) -> &str {
68+
"/v3/blocks/height/:block_height"
69+
}
70+
71+
/// Try to decode this request.
72+
/// There's nothing to load here, so just make sure the request is well-formed.
73+
fn try_parse_request(
74+
&mut self,
75+
preamble: &HttpRequestPreamble,
76+
captures: &Captures,
77+
query: Option<&str>,
78+
_body: &[u8],
79+
) -> Result<HttpRequestContents, Error> {
80+
if preamble.get_content_length() != 0 {
81+
return Err(Error::DecodeError(
82+
"Invalid Http request: expected 0-length body".to_string(),
83+
));
84+
}
85+
86+
let block_height_str = captures
87+
.name("block_height")
88+
.ok_or_else(|| {
89+
Error::DecodeError("Failed to match path to block height group".to_string())
90+
})?
91+
.as_str();
92+
93+
let block_height = block_height_str.parse::<u64>().map_err(|_| {
94+
Error::DecodeError("Invalid path: unparseable block height".to_string())
95+
})?;
96+
self.block_height = Some(block_height);
97+
98+
Ok(HttpRequestContents::new().query_string(query))
99+
}
100+
}
101+
102+
impl RPCRequestHandler for RPCNakamotoBlockByHeightRequestHandler {
103+
/// Reset internal state
104+
fn restart(&mut self) {
105+
self.block_height = None;
106+
}
107+
108+
/// Make the response
109+
fn try_handle_request(
110+
&mut self,
111+
preamble: HttpRequestPreamble,
112+
contents: HttpRequestContents,
113+
node: &mut StacksNodeState,
114+
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
115+
let block_height = self
116+
.block_height
117+
.take()
118+
.ok_or(NetError::SendError("Missing `block_height`".into()))?;
119+
120+
let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
121+
Ok(tip) => tip,
122+
Err(error_resp) => {
123+
return error_resp.try_into_contents().map_err(NetError::from);
124+
}
125+
};
126+
127+
let index_block_hash_res =
128+
node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
129+
chainstate
130+
.index_conn()
131+
.get_ancestor_block_hash(block_height, &tip)
132+
});
133+
134+
let block_id = match index_block_hash_res {
135+
Ok(index_block_hash_opt) => match index_block_hash_opt {
136+
Some(index_block_hash) => index_block_hash,
137+
None => {
138+
// block hash not found
139+
let msg = format!("No such block #{:?}\n", block_height);
140+
warn!("{}", &msg);
141+
return StacksHttpResponse::new_error(&preamble, &HttpNotFound::new(msg))
142+
.try_into_contents()
143+
.map_err(NetError::from);
144+
}
145+
},
146+
Err(e) => {
147+
// error querying the db
148+
let msg = format!("Failed to load block #{}: {:?}\n", block_height, &e);
149+
warn!("{}", &msg);
150+
return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg))
151+
.try_into_contents()
152+
.map_err(NetError::from);
153+
}
154+
};
155+
156+
let stream_res =
157+
node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
158+
let Some((tenure_id, parent_block_id)) = chainstate
159+
.nakamoto_blocks_db()
160+
.get_tenure_and_parent_block_id(&block_id)?
161+
else {
162+
return Err(ChainError::NoSuchBlockError);
163+
};
164+
NakamotoBlockStream::new(chainstate, block_id, tenure_id, parent_block_id)
165+
});
166+
167+
// start loading up the block
168+
let stream = match stream_res {
169+
Ok(stream) => stream,
170+
Err(ChainError::NoSuchBlockError) => {
171+
return StacksHttpResponse::new_error(
172+
&preamble,
173+
&HttpNotFound::new(format!("No such block #{:?}\n", &block_height)),
174+
)
175+
.try_into_contents()
176+
.map_err(NetError::from)
177+
}
178+
Err(e) => {
179+
// nope -- error trying to check
180+
let msg = format!("Failed to load block #{}: {:?}\n", block_height, &e);
181+
warn!("{}", &msg);
182+
return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg))
183+
.try_into_contents()
184+
.map_err(NetError::from);
185+
}
186+
};
187+
188+
let resp_preamble = HttpResponsePreamble::from_http_request_preamble(
189+
&preamble,
190+
200,
191+
"OK",
192+
None,
193+
HttpContentType::Bytes,
194+
);
195+
196+
Ok((
197+
resp_preamble,
198+
HttpResponseContents::from_stream(Box::new(stream)),
199+
))
200+
}
201+
}
202+
203+
/// Decode the HTTP response
204+
impl HttpResponse for RPCNakamotoBlockByHeightRequestHandler {
205+
/// Decode this response from a byte stream. This is called by the client to decode this
206+
/// message
207+
fn try_parse_response(
208+
&self,
209+
preamble: &HttpResponsePreamble,
210+
body: &[u8],
211+
) -> Result<HttpResponsePayload, Error> {
212+
let bytes = parse_bytes(preamble, body, MAX_MESSAGE_LEN.into())?;
213+
Ok(HttpResponsePayload::Bytes(bytes))
214+
}
215+
}
216+
217+
impl StacksHttpRequest {
218+
pub fn new_get_nakamoto_block_by_height(
219+
host: PeerHost,
220+
block_height: u64,
221+
tip: TipRequest,
222+
) -> StacksHttpRequest {
223+
StacksHttpRequest::new_for_peer(
224+
host,
225+
"GET".into(),
226+
format!("/v3/blocks/height/{}", block_height),
227+
HttpRequestContents::new().for_tip(tip),
228+
)
229+
.expect("FATAL: failed to construct request from infallible data")
230+
}
231+
}

stackslib/src/net/api/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub mod getattachment;
4242
pub mod getattachmentsinv;
4343
pub mod getblock;
4444
pub mod getblock_v3;
45+
pub mod getblockbyheight;
4546
pub mod getconstantval;
4647
pub mod getcontractabi;
4748
pub mod getcontractsrc;
@@ -92,6 +93,7 @@ impl StacksHttp {
9293
self.register_rpc_endpoint(getattachmentsinv::RPCGetAttachmentsInvRequestHandler::new());
9394
self.register_rpc_endpoint(getblock::RPCBlocksRequestHandler::new());
9495
self.register_rpc_endpoint(getblock_v3::RPCNakamotoBlockRequestHandler::new());
96+
self.register_rpc_endpoint(getblockbyheight::RPCNakamotoBlockByHeightRequestHandler::new());
9597
self.register_rpc_endpoint(getconstantval::RPCGetConstantValRequestHandler::new());
9698
self.register_rpc_endpoint(getcontractabi::RPCGetContractAbiRequestHandler::new());
9799
self.register_rpc_endpoint(getcontractsrc::RPCGetContractSrcRequestHandler::new());

0 commit comments

Comments
 (0)