Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: rpc blockbyheight #5445

Merged
merged 34 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dfee8f4
added getblockbyhash and getblockbyheight RPC api calls
rdeioris Nov 9, 2024
a98fdb6
removed useless get_nakamoto_block_by_hash_rowid and added notes abou…
rdeioris Nov 11, 2024
c7c899e
updated docs for /v3/blockbyhash and /v3/blockbyheight
rdeioris Nov 11, 2024
24a9c61
updated changelog
rdeioris Nov 11, 2024
94b85c3
added integration tests for blockbyhash and blockbyheight
rdeioris Nov 11, 2024
4a6276f
added tests::nakamoto_integrations::v3_blockbyhash_api_endpoint and t…
rdeioris Nov 11, 2024
8d46202
reformatted nakamoto_integrations.rs
rdeioris Nov 11, 2024
cb97c2d
reformat unit tests
rdeioris Nov 11, 2024
d46bfcd
fixed blockbyheight to use the marf
rdeioris Nov 12, 2024
6cc44c6
removed get_block_id_by_block_height over staging db
rdeioris Nov 12, 2024
ce533e0
more restful path for blockbyheight
rdeioris Nov 12, 2024
1e628f7
removed getblockbyhash
rdeioris Nov 13, 2024
4bea12a
removed files for getblockbyhash
rdeioris Nov 13, 2024
6acfddd
reverted getblock_v3 patch
rdeioris Nov 13, 2024
f81749a
removed test for old getblockbyhash
rdeioris Nov 13, 2024
67dc987
improved docs as suggested by jcnelson
rdeioris Nov 14, 2024
d778b2f
improved integration test for get_v3_block_by_height
rdeioris Nov 14, 2024
cc064a5
removed useless .clone()
rdeioris Nov 14, 2024
5cc1a1c
fixed typos in tests comments
rdeioris Nov 15, 2024
c0f53ff
added /v3/blocks/height/{block_height} in openapi definitions
rdeioris Nov 15, 2024
2e6d279
added block_id test over canonical_tip in test_try_make_response
rdeioris Nov 19, 2024
b83e2dc
merged with develop
rdeioris Nov 19, 2024
2cd02d6
added getblockbyhash and getblockbyheight RPC api calls
rdeioris Nov 9, 2024
fc2e4c3
removed useless get_nakamoto_block_by_hash_rowid and added notes abou…
rdeioris Nov 11, 2024
1535f9b
reformat unit tests
rdeioris Nov 11, 2024
5a812fa
fixed blockbyheight to use the marf
rdeioris Nov 12, 2024
553ec68
removed get_block_id_by_block_height over staging db
rdeioris Nov 12, 2024
cdf6879
removed getblockbyhash
rdeioris Nov 13, 2024
bc0fc2e
removed files for getblockbyhash
rdeioris Nov 13, 2024
648c9a9
reverted getblock_v3 patch
rdeioris Nov 13, 2024
5647bc9
fixed rebasing for CHANGELOG and bitcoin-tests
rdeioris Nov 19, 2024
0a4252f
upodated CHANGELOG and integration tests
rdeioris Nov 19, 2024
cd391dc
completed rebase to develop
rdeioris Nov 19, 2024
93d9536
Merge branch 'develop' into feat/rpc_blockbyhash_blockbyheight_develop
hstove Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ jobs:
- tests::nakamoto_integrations::v3_signer_api_endpoint
- tests::nakamoto_integrations::signer_chainstate
- tests::nakamoto_integrations::clarity_cost_spend_down
- tests::nakamoto_integrations::v3_blockbyheight_api_endpoint
# TODO: enable these once v1 signer is supported by a new nakamoto epoch
# - tests::signer::v1::dkg
# - tests::signer::v1::sign_request_rejected
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
- Add index to `metadata_table` in Clarity DB on `blockhash`
- 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.
- 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.
- Add `/v3/blocks/height/:block_height` rpc endpoint

## [3.0.0.0.1]

Expand Down
11 changes: 11 additions & 0 deletions docs/rpc-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,17 @@ data.

This will return 404 if the block does not exist.

### GET /v3/blocks/height/[Block Height]

Fetch a Nakamoto block given its block height. This returns the raw block
data.

This will return 404 if the block does not exist.

This endpoint also accepts a querystring parameter `?tip=` which when supplied
will return the block relative to the specified tip allowing the querying of
sibling blocks (same height, different tip) too.

### GET /v3/tenures/[Block ID]

Fetch a Nakamoto block and all of its ancestors in the same tenure, given its
Expand Down
34 changes: 34 additions & 0 deletions docs/rpc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,40 @@ paths:
content:
application/text-plain: {}

/v3/blocks/height/{block_height}:
get:
summary: Fetch a Nakamoto block by its height and optional tip
tags:
- Blocks
operationId: get_block_v3_by_height
description:
Fetch a Nakamoto block by its height and optional tip.
parameters:
- name: block_height
in: path
description: The block's height
required: true
schema:
type: integer
- name: tip
in: query
schema:
type: string
description: The Stacks chain tip to query from. If tip == latest or empty, the query will be run
from the latest known tip.
responses:
"200":
description: The raw SIP-003-encoded block will be returned.
content:
application/octet-stream:
schema:
type: string
format: binary
"404":
description: The block could not be found
content:
application/text-plain: {}

/v3/tenures/info:
get:
summary: Fetch metadata about the ongoing Nakamoto tenure
Expand Down
231 changes: 231 additions & 0 deletions stackslib/src/net/api/getblockbyheight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
// Copyright (C) 2020-2024 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 <http://www.gnu.org/licenses/>.

use std::io::{Read, Seek, SeekFrom, Write};
use std::{fs, io};

use regex::{Captures, Regex};
use rusqlite::Connection;
use serde::de::Error as de_Error;
use stacks_common::codec::{StacksMessageCodec, MAX_MESSAGE_LEN};
use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId};
use stacks_common::types::net::PeerHost;
use {serde, serde_json};

use crate::chainstate::nakamoto::{
NakamotoBlock, NakamotoChainState, NakamotoStagingBlocksConn, StacksDBIndexed,
};
use crate::chainstate::stacks::db::StacksChainState;
use crate::chainstate::stacks::Error as ChainError;
use crate::net::api::getblock_v3::{NakamotoBlockStream, RPCNakamotoBlockRequestHandler};
use crate::net::http::{
parse_bytes, Error, HttpBadRequest, HttpChunkGenerator, HttpContentType, HttpNotFound,
HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse, HttpResponseContents,
HttpResponsePayload, HttpResponsePreamble, HttpServerError, HttpVersion,
};
use crate::net::httpcore::{
HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp, StacksHttpRequest,
StacksHttpResponse,
};
use crate::net::{Error as NetError, StacksNodeState, TipRequest, MAX_HEADERS};
use crate::util_lib::db::{DBConn, Error as DBError};

#[derive(Clone)]
pub struct RPCNakamotoBlockByHeightRequestHandler {
pub block_height: Option<u64>,
}

impl RPCNakamotoBlockByHeightRequestHandler {
pub fn new() -> Self {
Self { block_height: None }
}
}

/// Decode the HTTP request
impl HttpRequest for RPCNakamotoBlockByHeightRequestHandler {
fn verb(&self) -> &'static str {
"GET"
}

fn path_regex(&self) -> Regex {
Regex::new(r#"^/v3/blocks/height/(?P<block_height>[0-9]{1,20})$"#).unwrap()
}

fn metrics_identifier(&self) -> &str {
"/v3/blocks/height/:block_height"
}

/// 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<HttpRequestContents, Error> {
if preamble.get_content_length() != 0 {
return Err(Error::DecodeError(
"Invalid Http request: expected 0-length body".to_string(),
));
}

let block_height_str = captures
.name("block_height")
.ok_or_else(|| {
Error::DecodeError("Failed to match path to block height group".to_string())
})?
.as_str();

let block_height = block_height_str.parse::<u64>().map_err(|_| {
Error::DecodeError("Invalid path: unparseable block height".to_string())
})?;
self.block_height = Some(block_height);

Ok(HttpRequestContents::new().query_string(query))
}
}

impl RPCRequestHandler for RPCNakamotoBlockByHeightRequestHandler {
/// Reset internal state
fn restart(&mut self) {
self.block_height = None;
}

/// Make the response
fn try_handle_request(
&mut self,
preamble: HttpRequestPreamble,
contents: HttpRequestContents,
node: &mut StacksNodeState,
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
let block_height = self
.block_height
.take()
.ok_or(NetError::SendError("Missing `block_height`".into()))?;

let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
Ok(tip) => tip,
Err(error_resp) => {
return error_resp.try_into_contents().map_err(NetError::from);
}
};

let index_block_hash_res =
node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
chainstate
.index_conn()
.get_ancestor_block_hash(block_height, &tip)
});

let block_id = match index_block_hash_res {
Ok(index_block_hash_opt) => match index_block_hash_opt {
Some(index_block_hash) => index_block_hash,
None => {
// block hash not found
let msg = format!("No such block #{:?}\n", block_height);
warn!("{}", &msg);
return StacksHttpResponse::new_error(&preamble, &HttpNotFound::new(msg))
.try_into_contents()
.map_err(NetError::from);
}
},
Err(e) => {
// error querying the db
let msg = format!("Failed to load block #{}: {:?}\n", block_height, &e);
warn!("{}", &msg);
return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg))
.try_into_contents()
.map_err(NetError::from);
}
};

let stream_res =
node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
let Some((tenure_id, parent_block_id)) = chainstate
.nakamoto_blocks_db()
.get_tenure_and_parent_block_id(&block_id)?
rdeioris marked this conversation as resolved.
Show resolved Hide resolved
else {
return Err(ChainError::NoSuchBlockError);
};
NakamotoBlockStream::new(chainstate, block_id, tenure_id, parent_block_id)
});

// start loading up the block
let stream = match stream_res {
Ok(stream) => stream,
Err(ChainError::NoSuchBlockError) => {
return StacksHttpResponse::new_error(
&preamble,
&HttpNotFound::new(format!("No such block #{:?}\n", &block_height)),
)
.try_into_contents()
.map_err(NetError::from)
}
Err(e) => {
// nope -- error trying to check
let msg = format!("Failed to load block #{}: {:?}\n", block_height, &e);
warn!("{}", &msg);
return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg))
.try_into_contents()
.map_err(NetError::from);
}
};

let resp_preamble = HttpResponsePreamble::from_http_request_preamble(
&preamble,
200,
"OK",
None,
HttpContentType::Bytes,
);

Ok((
resp_preamble,
HttpResponseContents::from_stream(Box::new(stream)),
))
}
}

/// Decode the HTTP response
impl HttpResponse for RPCNakamotoBlockByHeightRequestHandler {
/// Decode this response from a byte stream. This is called by the client to decode this
/// message
fn try_parse_response(
&self,
preamble: &HttpResponsePreamble,
body: &[u8],
) -> Result<HttpResponsePayload, Error> {
let bytes = parse_bytes(preamble, body, MAX_MESSAGE_LEN.into())?;
Ok(HttpResponsePayload::Bytes(bytes))
}
}

impl StacksHttpRequest {
pub fn new_get_nakamoto_block_by_height(
host: PeerHost,
block_height: u64,
tip: TipRequest,
) -> StacksHttpRequest {
StacksHttpRequest::new_for_peer(
host,
"GET".into(),
format!("/v3/blocks/height/{}", block_height),
HttpRequestContents::new().for_tip(tip),
)
.expect("FATAL: failed to construct request from infallible data")
}
}
2 changes: 2 additions & 0 deletions stackslib/src/net/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod getattachment;
pub mod getattachmentsinv;
pub mod getblock;
pub mod getblock_v3;
pub mod getblockbyheight;
pub mod getconstantval;
pub mod getcontractabi;
pub mod getcontractsrc;
Expand Down Expand Up @@ -92,6 +93,7 @@ impl StacksHttp {
self.register_rpc_endpoint(getattachmentsinv::RPCGetAttachmentsInvRequestHandler::new());
self.register_rpc_endpoint(getblock::RPCBlocksRequestHandler::new());
self.register_rpc_endpoint(getblock_v3::RPCNakamotoBlockRequestHandler::new());
self.register_rpc_endpoint(getblockbyheight::RPCNakamotoBlockByHeightRequestHandler::new());
self.register_rpc_endpoint(getconstantval::RPCGetConstantValRequestHandler::new());
self.register_rpc_endpoint(getcontractabi::RPCGetContractAbiRequestHandler::new());
self.register_rpc_endpoint(getcontractsrc::RPCGetContractSrcRequestHandler::new());
Expand Down
Loading