Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
101 changes: 77 additions & 24 deletions stackslib/src/net/api/gettenureblocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ use serde_json;
use stacks_common::types::chainstate::{BlockHeaderHash, ConsensusHash};
use stacks_common::types::net::PeerHost;

use crate::chainstate::burn::db::sortdb::SortitionDB;
use crate::chainstate::burn::db::DBConn;
use crate::chainstate::nakamoto::NakamotoChainState;
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState};
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo};
use crate::chainstate::stacks::Error as ChainError;
use crate::net::http::{
parse_json, Error, HttpChunkGenerator, HttpNotFound, HttpRequest, HttpRequestContents,
Expand All @@ -31,6 +32,64 @@ use crate::net::http::{
use crate::net::httpcore::{request, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse};
use crate::net::{Error as NetError, StacksNodeState};

/// Performs a MARF lookup to find the last snapshot with a sortition prior to the given burn block height
/// Will return an empty consensus hash if no prior sorititon exists (i.e., this is the Genesis tenure)
pub fn get_last_sortition_consensus_hash(
sortdb: &SortitionDB,
burn_block_height: u32,
preamble: &HttpRequestPreamble,
) -> Result<ConsensusHash, StacksHttpResponse> {
let handle = sortdb.index_handle_at_tip();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of loading from the sortition DB's highest known canonical tip, can you instead query relative to the sortition identified by the request's consensus hash? That way, it's unambiguous as to what parent consensus hash is being queried (for example, although unlikely, it is possible that the requested sortition and its parent are both non-canonical, and this change will ensure that the correct parent consensus hash is loaded).

Copy link
Contributor Author

@jacinta-stacks jacinta-stacks Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c37416b. And then redone in 5370994


// search backwards from the chain tip on the canonical fork to find the sortition
// that occurred before this burn block height.
match handle.get_last_snapshot_with_sortition(burn_block_height.saturating_sub(1).into()) {
Ok(last_sortition) => Ok(last_sortition.consensus_hash),
Err(e) => {
let msg = format!("Failed to get last sortition at block '{burn_block_height}': {e:?}");
error!("{msg}");
Err(StacksHttpResponse::new_error(
preamble,
&HttpServerError::new(msg),
))
}
}
}

/// Helper function to create RPCTenure from header info
pub fn create_rpc_tenure(
header_info: &StacksHeaderInfo,
last_sortition_ch: ConsensusHash,
) -> RPCTenure {
RPCTenure {
last_sortition_ch,
consensus_hash: header_info.consensus_hash.clone(),
burn_block_height: header_info.burn_header_height.into(),
burn_block_hash: header_info.burn_header_hash.to_hex(),
stacks_blocks: vec![],
}
}

/// Helper function to create tenure stream response
pub fn create_tenure_stream_response(
chainstate: &StacksChainState,
header_info: StacksHeaderInfo,
tenure: RPCTenure,
preamble: &HttpRequestPreamble,
) -> Result<RPCTenureStream, StacksHttpResponse> {
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}");
Err(StacksHttpResponse::new_error(
preamble,
&HttpServerError::new(msg),
))
}
}
}

#[derive(Clone)]
pub struct RPCNakamotoTenureBlocksRequestHandler {
pub(crate) consensus_hash: Option<ConsensusHash>,
Expand All @@ -56,6 +115,9 @@ pub struct RPCTenureBlock {
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct RPCTenure {
pub consensus_hash: ConsensusHash,
/// The consensus hash corresponding to the last successful sortition prior to this tenure.
/// If no prior sorititions exist, it will be the Genesis tenure consensus hash.
pub last_sortition_ch: ConsensusHash,
pub burn_block_height: u64,
pub burn_block_hash: String,
pub stacks_blocks: Vec<RPCTenureBlock>,
Expand Down Expand Up @@ -88,9 +150,10 @@ impl RPCTenureStream {
) -> Result<Self, ChainError> {
let headers_conn = chainstate.reopen_db()?;
let consensus_hash = tenure.consensus_hash;
let last_sortition_ch = tenure.last_sortition_ch;
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\": [");
let tenure_first_chunk = format!("{{\"consensus_hash\": \"{consensus_hash}\", \"last_sortition_ch\": \"{last_sortition_ch}\", \"burn_block_height\": {burn_block_height}, \"burn_block_hash\": \"{burn_block_hash}\", \"stacks_blocks\": [");
Ok(RPCTenureStream {
headers_conn,
consensus_hash,
Expand Down Expand Up @@ -263,8 +326,8 @@ impl RPCRequestHandler for RPCNakamotoTenureBlocksRequestHandler {
}
Err(e) => {
let msg = format!(
"Failed to query tenure blocks by consensus '{consensus_hash}': {e:?}"
);
"Failed to query tenure blocks by consensus '{consensus_hash}': {e:?}"
);
error!("{msg}");
return Err(StacksHttpResponse::new_error(
&preamble,
Expand All @@ -273,31 +336,21 @@ impl RPCRequestHandler for RPCNakamotoTenureBlocksRequestHandler {
}
};

let tenure = RPCTenure {
consensus_hash: header_info.consensus_hash.clone(),
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 last_sortition_ch = get_last_sortition_consensus_hash(
&sortdb,
header_info.burn_header_height,
&preamble,
)?;

let tenure = create_rpc_tenure(&header_info, last_sortition_ch);

create_tenure_stream_response(chainstate, header_info, tenure, &preamble)
});

let stream = match stream_res {
Ok(stream) => stream,
Err(e) => {
let msg = format!("Failed to create tenure stream: {e:?}");
error!("{msg}");
error!("Failed to create tenure stream: {e:?}");
return e.into();
}
};
Expand Down
34 changes: 13 additions & 21 deletions stackslib/src/net/api/gettenureblocksbyhash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ use stacks_common::types::net::PeerHost;

use crate::chainstate::nakamoto::NakamotoChainState;
use crate::chainstate::stacks::Error as ChainstateError;
use crate::net::api::gettenureblocks::{RPCTenure, RPCTenureStream};
use crate::net::api::gettenureblocks::{
create_rpc_tenure, create_tenure_stream_response, get_last_sortition_consensus_hash, RPCTenure,
};
use crate::net::http::{
parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
Expand Down Expand Up @@ -122,31 +124,21 @@ impl RPCRequestHandler for RPCNakamotoTenureBlocksByHashRequestHandler {
}
};

let tenure = RPCTenure {
consensus_hash: header_info.consensus_hash.clone(),
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 last_sortition_ch = get_last_sortition_consensus_hash(
&sortdb,
header_info.burn_header_height,
&preamble,
)?;

let tenure = create_rpc_tenure(&header_info, last_sortition_ch);

create_tenure_stream_response(chainstate, header_info, tenure, &preamble)
});

let stream = match stream_res {
Ok(stream) => stream,
Err(e) => {
let msg = format!("Failed to create tenure stream: {e:?}");
error!("{msg}");
error!("Failed to create tenure stream: {e:?}");
return e.into();
}
};
Expand Down
33 changes: 12 additions & 21 deletions stackslib/src/net/api/gettenureblocksbyheight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use stacks_common::types::net::PeerHost;

use crate::chainstate::nakamoto::NakamotoChainState;
use crate::chainstate::stacks::Error as ChainstateError;
use crate::net::api::gettenureblocks::{RPCTenure, RPCTenureStream};
use crate::net::api::gettenureblocks::{
create_rpc_tenure, create_tenure_stream_response, get_last_sortition_consensus_hash, RPCTenure,
};
use crate::net::http::{
parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
Expand Down Expand Up @@ -121,32 +123,21 @@ impl RPCRequestHandler for RPCNakamotoTenureBlocksByHeightRequestHandler {
));
}
};
let last_sortition_ch = get_last_sortition_consensus_hash(
&sortdb,
header_info.burn_header_height,
&preamble,
)?;

let tenure = create_rpc_tenure(&header_info, last_sortition_ch);

let tenure = RPCTenure {
consensus_hash: header_info.consensus_hash.clone(),
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),
));
}
}
create_tenure_stream_response(chainstate, header_info, tenure, &preamble)
});

let stream = match stream_res {
Ok(stream) => stream,
Err(e) => {
let msg = format!("Failed to create tenure stream: {e:?}");
error!("{msg}");
error!("Failed to create tenure stream: {e:?}");
return e.into();
}
};
Expand Down
74 changes: 70 additions & 4 deletions stackslib/src/net/api/tests/gettenureblocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
//
// 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::net::{IpAddr, Ipv4Addr, SocketAddr};

use stacks_common::types::chainstate::ConsensusHash;
Expand All @@ -25,6 +24,47 @@ use crate::net::httpcore::{StacksHttp, StacksHttpRequest};
use crate::net::test::TestEventObserver;
use crate::net::ProtocolFamily;

// A helper function to find two tenures with empty sortitions in between
pub fn find_sortitions_with_empty_sortitions_between(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we guaranteed to always have this case in the test setup?

rpc_test: &mut TestRPC,
) -> (ConsensusHash, ConsensusHash) {
// Find two tenures with empty sortitions in bewteen
let snapshots = rpc_test.peer_1.sortdb().get_all_snapshots().unwrap();

let mut first_sortition: Option<&_> = None;
let mut saw_non_sortition_between = false;

let mut result: Option<(&_, &_)> = None;

for s in snapshots.iter() {
if s.sortition {
match first_sortition {
None => {
first_sortition = Some(s);
saw_non_sortition_between = false;
}
Some(prev) => {
if saw_non_sortition_between {
// Found: sortition -> non-sortition(s) -> sortition
result = Some((prev, s));
break;
} else {
// restart window
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you also need to clear consensus_hashes_between here?

first_sortition = Some(s);
saw_non_sortition_between = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant, we already know this is false here.

}
}
}
} else if first_sortition.is_some() {
saw_non_sortition_between = true;
}
}

let (first, second) = result
.expect("Did not find sortition, non-sortition(s), sortition pattern required for test");
(first.consensus_hash.clone(), second.consensus_hash.clone())
}

#[test]
fn test_try_parse_request() {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
Expand Down Expand Up @@ -60,7 +100,7 @@ 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 mut rpc_test = TestRPC::setup_nakamoto(function_name!(), &test_observer);

let nakamoto_consensus_hash = rpc_test.consensus_hash.clone();

Expand Down Expand Up @@ -89,6 +129,11 @@ fn test_try_make_response() {
StacksHttpRequest::new_get_tenure_blocks(addr.clone().into(), &ConsensusHash([0x01; 20]));
requests.push(request);

// query tenure with empty sortitions in between
let (first, second) = find_sortitions_with_empty_sortitions_between(&mut rpc_test);
let request = StacksHttpRequest::new_get_tenure_blocks(addr.clone().into(), &second);
requests.push(request);

let mut responses = rpc_test.run(requests);

// got the Nakamoto tip
Expand All @@ -97,9 +142,13 @@ fn test_try_make_response() {
"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);
assert_ne!(
resp.last_sortition_ch, genesis_consensus_hash,
"Nakamoto tenure's last_sortition_ch should point to the previous winning sortition"
);

let mut blocks_index = 0;
for block in test_observer.get_blocks().iter().rev() {
if block.metadata.consensus_hash != nakamoto_consensus_hash {
Expand Down Expand Up @@ -133,6 +182,12 @@ fn test_try_make_response() {
let resp = response.decode_tenure_blocks().unwrap();
assert_eq!(resp.consensus_hash, genesis_consensus_hash);

// genesis/Epoch2 tenure has no parent tenure. Should return an empty consensus hash.
assert_eq!(
resp.last_sortition_ch,
ConsensusHash::from_bytes(&[0u8; 20]).unwrap(),
);

let blocks = test_observer.get_blocks();

let block = blocks.first().unwrap();
Expand All @@ -153,6 +208,17 @@ fn test_try_make_response() {
std::str::from_utf8(&response.try_serialize().unwrap()).unwrap()
);

let (preamble, body) = response.destruct();
let (preamble, _body) = response.destruct();
assert_eq!(preamble.status_code, 404);

// got tenure with empty sortitions in between
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, second);
assert_eq!(resp.last_sortition_ch, first);
}
Loading
Loading