From fd4c2379986a6abbd8cb0aa920a512a613619df9 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 19 Sep 2024 16:12:38 -0500 Subject: [PATCH 1/5] feat: add a consolidated endpoint for current and prior sortitions --- stacks-signer/src/chainstate.rs | 35 +-- stacks-signer/src/client/stacks_client.rs | 50 ++++ stackslib/src/net/api/getsortition.rs | 229 ++++++++++++------ .../src/tests/nakamoto_integrations.rs | 2 +- 4 files changed, 207 insertions(+), 109 deletions(-) diff --git a/stacks-signer/src/chainstate.rs b/stacks-signer/src/chainstate.rs index 4012fd48a0..4bbb9741a5 100644 --- a/stacks-signer/src/chainstate.rs +++ b/stacks-signer/src/chainstate.rs @@ -25,7 +25,7 @@ use stacks_common::types::chainstate::{ConsensusHash, StacksPublicKey}; use stacks_common::util::hash::Hash160; use stacks_common::{info, warn}; -use crate::client::{ClientError, StacksClient}; +use crate::client::{ClientError, CurrentAndLastSortition, StacksClient}; use crate::config::SignerConfig; use crate::signerdb::{BlockState, SignerDb}; @@ -138,8 +138,6 @@ pub struct SortitionsView { pub last_sortition: Option, /// the current successful sortition (this corresponds to the "current" miner slot) pub cur_sortition: SortitionState, - /// the hash at which the sortitions view was fetched - pub latest_consensus_hash: ConsensusHash, /// configuration settings for evaluating proposals pub config: ProposalEvalConfig, } @@ -608,42 +606,21 @@ impl SortitionsView { config: ProposalEvalConfig, client: &StacksClient, ) -> Result { - let latest_state = client.get_latest_sortition()?; - let latest_ch = latest_state.consensus_hash; - - // figure out what cur_sortition will be set to. - // if the latest sortition wasn't successful, query the last one that was. - let latest_success = if latest_state.was_sortition { - latest_state - } else { - info!("Latest state wasn't a sortition: {latest_state:?}"); - let last_sortition_ch = latest_state - .last_sortition_ch - .as_ref() - .ok_or_else(|| ClientError::NoSortitionOnChain)?; - client.get_sortition(last_sortition_ch)? - }; - - // now, figure out what `last_sortition` will be set to. - let last_sortition = latest_success - .last_sortition_ch - .as_ref() - .map(|ch| client.get_sortition(ch)) - .transpose()?; + let CurrentAndLastSortition { + current_sortition, + last_sortition, + } = client.get_current_and_last_sortition()?; - let cur_sortition = SortitionState::try_from(latest_success)?; + let cur_sortition = SortitionState::try_from(current_sortition)?; let last_sortition = last_sortition .map(SortitionState::try_from) .transpose() .ok() .flatten(); - let latest_consensus_hash = latest_ch; - Ok(Self { cur_sortition, last_sortition, - latest_consensus_hash, config, }) } diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 6e3bab341e..e59438db9f 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -88,6 +88,15 @@ struct GetStackersErrorResp { err_msg: String, } +/// Result from fetching current and last sortition: +/// two sortition infos +pub struct CurrentAndLastSortition { + /// the latest winning sortition in the current burnchain fork + pub current_sortition: SortitionInfo, + /// the last winning sortition prior to `current_sortition`, if there was one + pub last_sortition: Option, +} + impl From<&GlobalConfig> for StacksClient { fn from(config: &GlobalConfig) -> Self { Self { @@ -484,6 +493,47 @@ impl StacksClient { Ok(tenures) } + /// Get the current winning sortition and the last winning sortition + pub fn get_current_and_last_sortition(&self) -> Result { + debug!("stacks_node_client: Getting current and prior sortition..."); + let path = format!("{}/latest_and_last", self.sortition_info_path()); + let timer = crate::monitoring::new_rpc_call_timer(&path, &self.http_origin); + let send_request = || { + self.stacks_node_client.get(&path).send().map_err(|e| { + warn!("Signer failed to request latest sortition"; "err" => ?e); + e + }) + }; + let response = send_request()?; + timer.stop_and_record(); + if !response.status().is_success() { + return Err(ClientError::RequestFailure(response.status())); + } + let mut info_list: VecDeque = response.json()?; + let Some(current_sortition) = info_list.pop_front() else { + return Err(ClientError::UnexpectedResponseFormat( + "Empty SortitionInfo returned".into(), + )); + }; + if !current_sortition.was_sortition { + return Err(ClientError::UnexpectedResponseFormat( + "'Current' SortitionInfo returned which was not a winning sortition".into(), + )); + } + let last_sortition = if current_sortition.last_sortition_ch.is_some() { + let Some(last_sortition) = info_list.pop_back() else { + return Err(ClientError::UnexpectedResponseFormat("'Current' SortitionInfo has `last_sortition_ch` field, but corresponding data not returned".into())); + }; + Some(last_sortition) + } else { + None + }; + Ok(CurrentAndLastSortition { + current_sortition, + last_sortition, + }) + } + /// Get the sortition information for the latest sortition pub fn get_latest_sortition(&self) -> Result { debug!("stacks_node_client: Getting latest sortition..."); diff --git a/stackslib/src/net/api/getsortition.rs b/stackslib/src/net/api/getsortition.rs index 5e0557ca26..7b594530c2 100644 --- a/stackslib/src/net/api/getsortition.rs +++ b/stackslib/src/net/api/getsortition.rs @@ -28,6 +28,7 @@ use stacks_common::util::HexError; use {serde, serde_json}; use crate::chainstate::burn::db::sortdb::SortitionDB; +use crate::chainstate::burn::BlockSnapshot; use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState, NakamotoStagingBlocksConn}; use crate::chainstate::stacks::db::StacksChainState; use crate::chainstate::stacks::Error as ChainError; @@ -51,10 +52,13 @@ pub enum QuerySpecifier { BurnchainHeaderHash(BurnchainHeaderHash), BlockHeight(u64), Latest, + /// Fetch the latest sortition *which was a winning sortition* and that sortition's + /// last sortition, returning two SortitionInfo structs. + LatestAndLast, } pub static RPC_SORTITION_INFO_PATH: &str = "/v3/sortitions"; -static PATH_REGEX: &str = "^/v3/sortitions(/(?P[a-z_]{1,15})/(?P[0-9a-f]{1,64}))?$"; +static PATH_REGEX: &str = "^/v3/sortitions(/(?P[a-z_]{1,15})(/(?P[0-9a-f]{1,64}))?)?$"; /// Struct for sortition information returned via the GetSortition API call #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] @@ -112,6 +116,7 @@ impl TryFrom<(&str, &str)> for QuerySpecifier { value.1 }; match value.0 { + "latest_and_last" => Ok(Self::LatestAndLast), "consensus" => Ok(Self::ConsensusHash( ConsensusHash::from_hex(hex_str).map_err(|e| Error::DecodeError(e.to_string()))?, )), @@ -141,6 +146,74 @@ impl GetSortitionHandler { query: QuerySpecifier::Latest, } } + + fn get_sortition_info( + sortition_sn: BlockSnapshot, + sortdb: &SortitionDB, + ) -> Result { + let (miner_pk_hash160, stacks_parent_ch, committed_block_hash, last_sortition_ch) = + if !sortition_sn.sortition { + let handle = sortdb.index_handle(&sortition_sn.sortition_id); + let last_sortition = + handle.get_last_snapshot_with_sortition(sortition_sn.block_height)?; + (None, None, None, Some(last_sortition.consensus_hash)) + } else { + let block_commit = SortitionDB::get_block_commit(sortdb.conn(), &sortition_sn.winning_block_txid, &sortition_sn.sortition_id)? + .ok_or_else(|| { + error!( + "Failed to load block commit from Sortition DB for snapshot with a winning block txid"; + "sortition_id" => %sortition_sn.sortition_id, + "txid" => %sortition_sn.winning_block_txid, + ); + ChainError::NoSuchBlockError + })?; + let handle = sortdb.index_handle(&sortition_sn.sortition_id); + let stacks_parent_sn = handle + .get_block_snapshot_by_height(block_commit.parent_block_ptr.into())? + .ok_or_else(|| { + warn!( + "Failed to load the snapshot of the winning block commits parent"; + "sortition_id" => %sortition_sn.sortition_id, + "txid" => %sortition_sn.winning_block_txid, + ); + ChainError::NoSuchBlockError + })?; + + // try to figure out what the last snapshot in this fork was with a successful + // sortition. + // optimization heuristic: short-circuit the load if its just `stacks_parent_sn` + let last_sortition_ch = if stacks_parent_sn.sortition { + stacks_parent_sn.consensus_hash.clone() + } else { + // we actually need to perform the marf lookup + let last_sortition = handle.get_last_snapshot_with_sortition( + sortition_sn.block_height.saturating_sub(1), + )?; + last_sortition.consensus_hash + }; + + ( + sortition_sn.miner_pk_hash.clone(), + Some(stacks_parent_sn.consensus_hash), + Some(block_commit.block_header_hash), + Some(last_sortition_ch), + ) + }; + + Ok(SortitionInfo { + burn_block_hash: sortition_sn.burn_header_hash, + burn_block_height: sortition_sn.block_height, + burn_header_timestamp: sortition_sn.burn_header_timestamp, + sortition_id: sortition_sn.sortition_id, + parent_sortition_id: sortition_sn.parent_sortition_id, + consensus_hash: sortition_sn.consensus_hash, + was_sortition: sortition_sn.sortition, + miner_pk_hash160, + stacks_parent_ch, + last_sortition_ch, + committed_block_hash, + }) + } } /// Decode the HTTP request impl HttpRequest for GetSortitionHandler { @@ -169,9 +242,15 @@ impl HttpRequest for GetSortitionHandler { let req_contents = HttpRequestContents::new().query_string(query); self.query = QuerySpecifier::Latest; - if let (Some(key), Some(value)) = (captures.name("key"), captures.name("value")) { - self.query = QuerySpecifier::try_from((key.as_str(), value.as_str()))?; - } + match (captures.name("key"), captures.name("value")) { + (Some(key), None) => { + self.query = QuerySpecifier::try_from((key.as_str(), ""))?; + } + (Some(key), Some(value)) => { + self.query = QuerySpecifier::try_from((key.as_str(), value.as_str()))?; + } + _ => {} + }; Ok(req_contents) } @@ -194,81 +273,37 @@ impl RPCRequestHandler for GetSortitionHandler { _contents: HttpRequestContents, node: &mut StacksNodeState, ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { - let result = - node.with_node_state(|network, sortdb, _chainstate, _mempool, _rpc_args| { - let query_result = match self.query { - QuerySpecifier::Latest => { + let result = node.with_node_state(|network, sortdb, _chainstate, _mempool, _rpc_args| { + let query_result = match self.query { + QuerySpecifier::Latest => Ok(Some(network.burnchain_tip.clone())), + QuerySpecifier::ConsensusHash(ref consensus_hash) => { + SortitionDB::get_block_snapshot_consensus(sortdb.conn(), consensus_hash) + } + QuerySpecifier::BurnchainHeaderHash(ref burn_hash) => { + let handle = sortdb.index_handle_at_tip(); + handle.get_block_snapshot(burn_hash) + } + QuerySpecifier::BlockHeight(burn_height) => { + let handle = sortdb.index_handle_at_tip(); + handle.get_block_snapshot_by_height(burn_height) + } + QuerySpecifier::LatestAndLast => { + if network.burnchain_tip.sortition { + // optimization: if the burn chain tip had a sortition, just return that Ok(Some(network.burnchain_tip.clone())) - }, - QuerySpecifier::ConsensusHash(ref consensus_hash) => { - SortitionDB::get_block_snapshot_consensus(sortdb.conn(), consensus_hash) - }, - QuerySpecifier::BurnchainHeaderHash(ref burn_hash) => { - let handle = sortdb.index_handle_at_tip(); - handle.get_block_snapshot(burn_hash) - }, - QuerySpecifier::BlockHeight(burn_height) => { - let handle = sortdb.index_handle_at_tip(); - handle.get_block_snapshot_by_height(burn_height) - }, - }; - let sortition_sn = query_result? - .ok_or_else(|| ChainError::NoSuchBlockError)?; - - let (miner_pk_hash160, stacks_parent_ch, committed_block_hash, last_sortition_ch) = if !sortition_sn.sortition { - let handle = sortdb.index_handle(&sortition_sn.sortition_id); - let last_sortition = handle.get_last_snapshot_with_sortition(sortition_sn.block_height)?; - (None, None, None, Some(last_sortition.consensus_hash)) - } else { - let block_commit = SortitionDB::get_block_commit(sortdb.conn(), &sortition_sn.winning_block_txid, &sortition_sn.sortition_id)? - .ok_or_else(|| { - error!( - "Failed to load block commit from Sortition DB for snapshot with a winning block txid"; - "sortition_id" => %sortition_sn.sortition_id, - "txid" => %sortition_sn.winning_block_txid, - ); - ChainError::NoSuchBlockError - })?; - let handle = sortdb.index_handle(&sortition_sn.sortition_id); - let stacks_parent_sn = handle.get_block_snapshot_by_height(block_commit.parent_block_ptr.into())? - .ok_or_else(|| { - warn!( - "Failed to load the snapshot of the winning block commits parent"; - "sortition_id" => %sortition_sn.sortition_id, - "txid" => %sortition_sn.winning_block_txid, - ); - ChainError::NoSuchBlockError - })?; - - // try to figure out what the last snapshot in this fork was with a successful - // sortition. - // optimization heuristic: short-circuit the load if its just `stacks_parent_sn` - let last_sortition_ch = if sortition_sn.num_sortitions == stacks_parent_sn.num_sortitions + 1 { - stacks_parent_sn.consensus_hash.clone() } else { - // we actually need to perform the marf lookup - let last_sortition = handle.get_last_snapshot_with_sortition(sortition_sn.block_height.saturating_sub(1))?; - last_sortition.consensus_hash - }; - - (sortition_sn.miner_pk_hash.clone(), Some(stacks_parent_sn.consensus_hash), Some(block_commit.block_header_hash), - Some(last_sortition_ch)) - }; - - Ok(SortitionInfo { - burn_block_hash: sortition_sn.burn_header_hash, - burn_block_height: sortition_sn.block_height, - burn_header_timestamp: sortition_sn.burn_header_timestamp, - sortition_id: sortition_sn.sortition_id, - parent_sortition_id: sortition_sn.parent_sortition_id, - consensus_hash: sortition_sn.consensus_hash, - was_sortition: sortition_sn.sortition, - miner_pk_hash160, - stacks_parent_ch, - last_sortition_ch, - committed_block_hash, - }) - }); + // we actually need to perform a marf lookup to find that last snapshot + // with a sortition + let handle = sortdb.index_handle_at_tip(); + let last_sortition = handle + .get_last_snapshot_with_sortition(network.burnchain_tip.block_height)?; + Ok(Some(last_sortition)) + } + } + }; + let sortition_sn = query_result?.ok_or_else(|| ChainError::NoSuchBlockError)?; + Self::get_sortition_info(sortition_sn, sortdb) + }); let block = match result { Ok(block) => block, @@ -290,8 +325,44 @@ impl RPCRequestHandler for GetSortitionHandler { } }; + let last_sortition_ch = block.last_sortition_ch.clone(); + let mut info_list = vec![block]; + if self.query == QuerySpecifier::LatestAndLast { + // if latest **and** last are requested, lookup the sortition info for last_sortition_ch + if let Some(last_sortition_ch) = last_sortition_ch { + let result = node.with_node_state(|_, sortdb, _, _, _| { + let last_sortition_sn = SortitionDB::get_block_snapshot_consensus( + sortdb.conn(), + &last_sortition_ch, + )? + .ok_or_else(|| ChainError::NoSuchBlockError)?; + Self::get_sortition_info(last_sortition_sn, sortdb) + }); + let last_block = match result { + Ok(block) => block, + Err(ChainError::NoSuchBlockError) => { + return StacksHttpResponse::new_error( + &preamble, + &HttpNotFound::new(format!("Could not find snapshot for the `last_sortition_ch`({last_sortition_ch})\n")), + ) + .try_into_contents() + .map_err(NetError::from) + } + Err(e) => { + // nope -- error trying to check + let msg = format!("Failed to load snapshot for `last_sortition_ch`({last_sortition_ch}): {:?}\n", &e); + warn!("{msg}"); + return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg)) + .try_into_contents() + .map_err(NetError::from); + } + }; + info_list.push(last_block); + } + } + let preamble = HttpResponsePreamble::ok_json(&preamble); - let result = HttpResponseContents::try_from_json(&block)?; + let result = HttpResponseContents::try_from_json(&info_list)?; Ok((preamble, result)) } } @@ -302,7 +373,7 @@ impl HttpResponse for GetSortitionHandler { preamble: &HttpResponsePreamble, body: &[u8], ) -> Result { - let sortition_info: SortitionInfo = parse_json(preamble, body)?; + let sortition_info: Vec = parse_json(preamble, body)?; Ok(HttpResponsePayload::try_from_json(sortition_info)?) } } diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 32924ab7b7..4057852c41 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -5506,7 +5506,7 @@ fn signer_chainstate() { let time_start = Instant::now(); let proposal = loop { let proposal = get_latest_block_proposal(&naka_conf, &sortdb).unwrap(); - if proposal.0.header.consensus_hash == sortitions_view.latest_consensus_hash { + if proposal.0.header.consensus_hash == sortitions_view.cur_sortition.consensus_hash { break proposal; } if time_start.elapsed() > Duration::from_secs(20) { From 77c1036bf96494a95198d5c7381dc74c8e5887b7 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 19 Sep 2024 20:45:14 -0500 Subject: [PATCH 2/5] chore: remove dead code, fix unit test build --- stacks-signer/src/client/stacks_client.rs | 43 +---------------------- stacks-signer/src/tests/chainstate.rs | 1 - 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index e59438db9f..09c0040aea 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -1,4 +1,3 @@ -use std::collections::VecDeque; // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation // Copyright (C) 2020-2024 Stacks Open Internet Foundation // @@ -14,6 +13,7 @@ use std::collections::VecDeque; // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::collections::VecDeque; use std::net::SocketAddr; use blockstack_lib::burnchains::Txid; @@ -534,47 +534,6 @@ impl StacksClient { }) } - /// Get the sortition information for the latest sortition - pub fn get_latest_sortition(&self) -> Result { - debug!("stacks_node_client: Getting latest sortition..."); - let path = self.sortition_info_path(); - let timer = crate::monitoring::new_rpc_call_timer(&path, &self.http_origin); - let send_request = || { - self.stacks_node_client.get(&path).send().map_err(|e| { - warn!("Signer failed to request latest sortition"; "err" => ?e); - e - }) - }; - let response = send_request()?; - timer.stop_and_record(); - if !response.status().is_success() { - return Err(ClientError::RequestFailure(response.status())); - } - let sortition_info = response.json()?; - Ok(sortition_info) - } - - /// Get the sortition information for a given sortition - pub fn get_sortition(&self, ch: &ConsensusHash) -> Result { - debug!("stacks_node_client: Getting sortition with consensus hash {ch}..."); - let path = format!("{}/consensus/{}", self.sortition_info_path(), ch.to_hex()); - let timer_label = format!("{}/consensus/:consensus_hash", self.sortition_info_path()); - let timer = crate::monitoring::new_rpc_call_timer(&timer_label, &self.http_origin); - let send_request = || { - self.stacks_node_client.get(&path).send().map_err(|e| { - warn!("Signer failed to request sortition"; "consensus_hash" => %ch, "err" => ?e); - e - }) - }; - let response = send_request()?; - timer.stop_and_record(); - if !response.status().is_success() { - return Err(ClientError::RequestFailure(response.status())); - } - let sortition_info = response.json()?; - Ok(sortition_info) - } - /// Get the current peer info data from the stacks node pub fn get_peer_info(&self) -> Result { debug!("stacks_node_client: Getting peer info..."); diff --git a/stacks-signer/src/tests/chainstate.rs b/stacks-signer/src/tests/chainstate.rs index a13ab24a59..53f60e9cfe 100644 --- a/stacks-signer/src/tests/chainstate.rs +++ b/stacks-signer/src/tests/chainstate.rs @@ -82,7 +82,6 @@ fn setup_test_environment( }); let view = SortitionsView { - latest_consensus_hash: cur_sortition.consensus_hash, cur_sortition, last_sortition, config: ProposalEvalConfig { From b396798c489361516d96bc432e390a415842fe88 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 19 Sep 2024 21:03:20 -0500 Subject: [PATCH 3/5] add /v3/sortitions to openapi.yaml --- .../api/core-node/get_sortitions.example.json | 15 +++++++ ...t_sortitions_latest_and_prior.example.json | 28 +++++++++++++ docs/rpc/openapi.yaml | 40 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 docs/rpc/api/core-node/get_sortitions.example.json create mode 100644 docs/rpc/api/core-node/get_sortitions_latest_and_prior.example.json diff --git a/docs/rpc/api/core-node/get_sortitions.example.json b/docs/rpc/api/core-node/get_sortitions.example.json new file mode 100644 index 0000000000..a56fd887b1 --- /dev/null +++ b/docs/rpc/api/core-node/get_sortitions.example.json @@ -0,0 +1,15 @@ +[ + { + "burn_block_hash": "0x046f54cd1924a5d80fc3b8186d0334b7521acae90f9e136e2bee680c720d0e83", + "burn_block_height": 231, + "burn_header_timestamp": 1726797570, + "sortition_id": "0x8a5116b7b4306dc4f6db290d1adfff9e1347f3e921bb793fc4c33e2ff05056e2", + "parent_sortition_id": "0xdaf479110cf859e58c56b6ae941f8a14e7c7992c57027183dfbda4a4b820897c", + "consensus_hash": "0x8d2c51db737597a93191f49bcdc9c7bb44b90892", + "was_sortition": true, + "miner_pk_hash160": "0x6bc51b33e9f3626944eb879147e18111581f8f9b", + "stacks_parent_ch": "0x697357c72da55b759b1d6b721676c92c69f0b490", + "last_sortition_ch": "0x697357c72da55b759b1d6b721676c92c69f0b490", + "committed_block_hash": "0xeea47d6d639c565027110e192e308fb11656183d5c077bcd718d830652800183" + } +] diff --git a/docs/rpc/api/core-node/get_sortitions_latest_and_prior.example.json b/docs/rpc/api/core-node/get_sortitions_latest_and_prior.example.json new file mode 100644 index 0000000000..db970637ed --- /dev/null +++ b/docs/rpc/api/core-node/get_sortitions_latest_and_prior.example.json @@ -0,0 +1,28 @@ +[ + { + "burn_block_hash": "0x046f54cd1924a5d80fc3b8186d0334b7521acae90f9e136e2bee680c720d0e83", + "burn_block_height": 231, + "burn_header_timestamp": 1726797570, + "sortition_id": "0x8a5116b7b4306dc4f6db290d1adfff9e1347f3e921bb793fc4c33e2ff05056e2", + "parent_sortition_id": "0xdaf479110cf859e58c56b6ae941f8a14e7c7992c57027183dfbda4a4b820897c", + "consensus_hash": "0x8d2c51db737597a93191f49bcdc9c7bb44b90892", + "was_sortition": true, + "miner_pk_hash160": "0x6bc51b33e9f3626944eb879147e18111581f8f9b", + "stacks_parent_ch": "0x697357c72da55b759b1d6b721676c92c69f0b490", + "last_sortition_ch": "0x697357c72da55b759b1d6b721676c92c69f0b490", + "committed_block_hash": "0xeea47d6d639c565027110e192e308fb11656183d5c077bcd718d830652800183" + }, + { + "burn_block_hash": "0x496ff02cb63a4850d0bdee5fab69284b6eb0392b4538e1c462f82362c5becfa4", + "burn_block_height": 230, + "burn_header_timestamp": 1726797570, + "sortition_id": "0xdaf479110cf859e58c56b6ae941f8a14e7c7992c57027183dfbda4a4b820897c", + "parent_sortition_id": "0xf9058692055cbd879d7f71e566e44b905a887b2b182407ed596b5d6499ceae2a", + "consensus_hash": "0x697357c72da55b759b1d6b721676c92c69f0b490", + "was_sortition": true, + "miner_pk_hash160": "0x6bc51b33e9f3626944eb879147e18111581f8f9b", + "stacks_parent_ch": "0xf7d1bd7d9d5c5a5c368402b6ef9510bd014d70f7", + "last_sortition_ch": "0xf7d1bd7d9d5c5a5c368402b6ef9510bd014d70f7", + "committed_block_hash": "0x36ee5f7f7271de1c1d4cd830e36320b51e01605547621267ae6e9b4e9b10f95e" + } +] diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml index 3d4249329e..e01a0956d1 100644 --- a/docs/rpc/openapi.yaml +++ b/docs/rpc/openapi.yaml @@ -675,3 +675,43 @@ paths: schema: type: string + /v3/sortitions/{lookup_kind}/{lookup}: + get: + summary: Fetch information about evaluated burnchain blocks (i.e., sortitions). + tags: + - Blocks + operationId: get_sortitions + description: + Fetch sortition information about a burnchain block. If the `lookup_kind` and `lookup` parameters are empty, it will return information about the latest burn block. + responses: + "200": + description: Information for the given reward cycle + content: + application/json: + example: + $ref: ./api/core-node/get_sortitions.example.json + "200": + description: Sortition information about the latest burn block with a winning miner, and the previous such burn block. + content: + application/json: + example: + $ref: ./api/core-node/get_sortitions_latest_and_prior.example.json + parameters: + - name: lookup_kind + in: path + description: |- + The style of lookup that should be performed. If not given, the most recent burn block processed will be returned. + Otherwise, the `lookup_kind` should be one of the following strings: + * `consensus` - find the burn block using the consensus hash supplied in the `lookup` field. + * `burn_height` - find the burn block using the burn block height supplied in the `lookup` field. + * `burn` - find the burn block using the burn block hash supplied in the `lookup` field. + * `latest_and_last` - return information about the latest burn block with a winning miner *and* the previous such burn block + required: false + schema: + type: string + - name: lookup + in: path + description: The value to use for the lookup if `lookup_kind` is `consensus`, `burn_height`, or `burn` + required: false + schema: + type: string From e857672a8924e76a540b96f0dc6722847274e6b0 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 19 Sep 2024 21:29:30 -0500 Subject: [PATCH 4/5] docs: correct multi-example openapi.yaml --- docs/rpc/openapi.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml index e01a0956d1..5547d3bcb6 100644 --- a/docs/rpc/openapi.yaml +++ b/docs/rpc/openapi.yaml @@ -685,17 +685,18 @@ paths: Fetch sortition information about a burnchain block. If the `lookup_kind` and `lookup` parameters are empty, it will return information about the latest burn block. responses: "200": - description: Information for the given reward cycle + description: Information for the burn block or in the case of `latest_and_last`, multiple burn blocks content: application/json: - example: - $ref: ./api/core-node/get_sortitions.example.json - "200": - description: Sortition information about the latest burn block with a winning miner, and the previous such burn block. - content: - application/json: - example: - $ref: ./api/core-node/get_sortitions_latest_and_prior.example.json + examples: + Latest: + description: A single element list is returned when just one sortition is requested + value: + $ref: ./api/core-node/get_sortitions.example.json + LatestAndLast: + description: Sortition information about the latest burn block with a winning miner, and the previous such burn block. + value: + $ref: ./api/core-node/get_sortitions_latest_and_prior.example.json parameters: - name: lookup_kind in: path From 5d860c60b7b2cdb6c69c9e1466028904d240394a Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Sun, 22 Sep 2024 07:03:22 -0500 Subject: [PATCH 5/5] test: add response unit test for getsortition --- stackslib/src/net/api/tests/getsortition.rs | 72 ++++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/stackslib/src/net/api/tests/getsortition.rs b/stackslib/src/net/api/tests/getsortition.rs index 8541b73eb6..e112fde4a0 100644 --- a/stackslib/src/net/api/tests/getsortition.rs +++ b/stackslib/src/net/api/tests/getsortition.rs @@ -19,10 +19,14 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use stacks_common::types::chainstate::{BurnchainHeaderHash, ConsensusHash}; use stacks_common::types::net::PeerHost; -use crate::net::api::getsortition::{GetSortitionHandler, QuerySpecifier}; +use crate::net::api::getsortition::{GetSortitionHandler, QuerySpecifier, SortitionInfo}; +use crate::net::api::tests::test_rpc; use crate::net::connection::ConnectionOptions; -use crate::net::http::{Error as HttpError, HttpRequestPreamble, HttpVersion}; -use crate::net::httpcore::{RPCRequestHandler, StacksHttp, StacksHttpPreamble}; +use crate::net::http::{ + Error as HttpError, HttpRequestContents, HttpRequestPreamble, HttpResponse, + HttpResponsePayload, HttpVersion, +}; +use crate::net::httpcore::{RPCRequestHandler, StacksHttp, StacksHttpPreamble, StacksHttpRequest}; use crate::net::Error as NetError; fn make_preamble(query: &str) -> HttpRequestPreamble { @@ -99,3 +103,65 @@ fn test_parse_request() { } } } + +#[test] +fn response() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 33333); + + let request = StacksHttpRequest::new_for_peer( + addr.into(), + "GET".into(), + "/v3/sortitions".into(), + HttpRequestContents::new(), + ) + .expect("FATAL: failed to construct request from infallible data"); + let mut responses = test_rpc(function_name!(), vec![request]); + let HttpResponsePayload::JSON(response) = + responses.pop().unwrap().get_http_payload_ok().unwrap() + else { + panic!("Expected JSON response"); + }; + + info!("Response:\n{:#?}\n", response); + + let info_array = response.as_array().expect("Response should be array"); + assert_eq!( + info_array.len(), + 1, + "/v3/sortitions should return a single entry" + ); + + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 33333); + let request = StacksHttpRequest::new_for_peer( + addr.into(), + "GET".into(), + "/v3/sortitions/latest_and_last".into(), + HttpRequestContents::new(), + ) + .expect("FATAL: failed to construct request from infallible data"); + let mut responses = test_rpc(function_name!(), vec![request]); + let HttpResponsePayload::JSON(response) = + responses.pop().unwrap().get_http_payload_ok().unwrap() + else { + panic!("Expected JSON response"); + }; + + info!("Response:\n{:#?}\n", response); + + let info_array = response.as_array().expect("Response should be array"); + assert_eq!( + info_array.len(), + 2, + "/v3/sortitions/latest_and_last should return 2 entries" + ); + let first_entry: SortitionInfo = serde_json::from_value(info_array[0].clone()) + .expect("Response array elements should parse to SortitionInfo"); + let second_entry: SortitionInfo = serde_json::from_value(info_array[1].clone()) + .expect("Response array elements should parse to SortitionInfo"); + assert!(first_entry.was_sortition); + assert!(second_entry.was_sortition); + assert_eq!( + first_entry.last_sortition_ch.as_ref().unwrap(), + &second_entry.consensus_hash, + ); +}