Skip to content

Commit 03d84fa

Browse files
authored
Merge pull request #6347 from Jiloc/fix/health-v3-use-canonical-stacks-height-header
fix: `/v3/health` use canonical stacks height header
2 parents 8f56fd4 + e4c57fb commit 03d84fa

File tree

15 files changed

+216
-954
lines changed

15 files changed

+216
-954
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"difference_from_max_peer": 0,
3+
"node_stacks_tip_height": 12345,
34
"max_stacks_height_of_neighbors": 12345,
4-
"node_stacks_tip_height": 12345
5+
"max_stacks_neighbor_address": "127.0.0.1:8080"
56
}

docs/rpc/components/schemas/get-health.schema.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ properties:
1616
type: integer
1717
minimum: 0
1818
description: The current Stacks tip height of this node
19+
max_stacks_neighbor_address:
20+
type: [string, "null"]
21+
description: The address of the most advanced peer
1922
description: Health information about the node's synchronization status

docs/rpc/openapi.yaml

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,24 +1295,11 @@ paths:
12951295
- `difference_from_max_peer`: The difference in Stacks height between this node and its most advanced peer.
12961296
- `max_stacks_height_of_neighbors`: The maximum Stacks height observed among the node"s connected peers.
12971297
- `node_stacks_tip_height`: The current Stacks tip height of this node.
1298+
- `max_stacks_neighbor_address`: The address of the most advanced peer. Null if no peer data is available.
12981299
tags:
12991300
- Info
13001301
security: []
13011302
operationId: getNodeHealth
1302-
parameters:
1303-
- in: query
1304-
name: neighbors
1305-
description: |
1306-
Specifies the set of peers to use for health checks.
1307-
- `initial` (default): Use only the initial bootstrap peers.
1308-
- `all`: Use all connected peers.
1309-
required: false
1310-
schema:
1311-
type: string
1312-
enum:
1313-
- initial
1314-
- all
1315-
default: initial
13161303
responses:
13171304
"200":
13181305
description: Success
@@ -1323,11 +1310,8 @@ paths:
13231310
example:
13241311
$ref: "./components/examples/node-health.example.json"
13251312
"400":
1326-
description: Bad request, such as an invalid `neighbors` query parameter.
13271313
$ref: "#/components/responses/BadRequest"
13281314
"500":
1329-
description: |
1330-
Failed to query for health (e.g., no data or no valid peers to query from).
13311315
$ref: "#/components/responses/InternalServerError"
13321316

13331317
/v2/attachments/{hash}:

stacks-node/src/tests/nakamoto_integrations.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ use crate::operations::BurnchainOpSigner;
126126
use crate::run_loop::boot_nakamoto;
127127
use crate::tests::neon_integrations::{
128128
call_read_only, get_account, get_account_result, get_chain_info_opt, get_chain_info_result,
129-
get_neighbors, get_pox_info, get_sortition_info, next_block_and_wait,
129+
get_neighbors, get_node_health, get_pox_info, get_sortition_info, next_block_and_wait,
130130
run_until_burnchain_height, submit_tx, submit_tx_fallible, test_observer, wait_for_runloop,
131131
};
132132
use crate::tests::signer::SignerTest;
@@ -2426,10 +2426,11 @@ fn mine_multiple_per_tenure_integration() {
24262426
/// It starts in Epoch 2.0, mines with `neon_node` to Epoch 3.0, and then switches
24272427
/// to Nakamoto operation (activating pox-4 by submitting a stack-stx tx). The BootLoop
24282428
/// struct handles the epoch-2/3 tear-down and spin-up.
2429-
/// This test makes three assertions:
2429+
/// This test makes four assertions:
24302430
/// * 15 tenures are mined after 3.0 starts
24312431
/// * Each tenure has 6 blocks (the coinbase block and 5 interim blocks)
24322432
/// * Both nodes see the same chainstate at the end of the test
2433+
/// * Both nodes have the same `PeerNetwork::highest_stacks_height_of_neighbors`
24332434
fn multiple_miners() {
24342435
if env::var("BITCOIND_TEST") != Ok("1".into()) {
24352436
return;
@@ -2657,6 +2658,19 @@ fn multiple_miners() {
26572658
info!("Peer height information"; "peer_1" => peer_1_height, "peer_2" => peer_2_height);
26582659
assert_eq!(peer_1_height, peer_2_height);
26592660

2661+
// check that the `ConversationHttp::chat` was called and updated
2662+
// `PeerNetwork::highest_stacks_height_of_neighbors`
2663+
wait_for(20, || {
2664+
let health_node_1 = get_node_health(&naka_conf);
2665+
let health_node_2 = get_node_health(&conf_node_2);
2666+
info!("Peer health information"; "peer_1" => ?health_node_1, "peer_2" => ?health_node_2);
2667+
Ok(
2668+
health_node_1.max_stacks_height_of_neighbors == peer_2_height
2669+
&& health_node_2.max_stacks_height_of_neighbors == peer_1_height,
2670+
)
2671+
})
2672+
.unwrap();
2673+
26602674
assert!(tip.anchored_header.as_stacks_nakamoto().is_some());
26612675
assert_eq!(
26622676
tip.stacks_block_height,

stacks-node/src/tests/neon_integrations.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ use stacks::core::{
5555
};
5656
use stacks::net::api::getaccount::AccountEntryResponse;
5757
use stacks::net::api::getcontractsrc::ContractSrcResponse;
58+
use stacks::net::api::gethealth::RPCGetHealthResponse;
5859
use stacks::net::api::getinfo::RPCPeerInfoData;
5960
use stacks::net::api::getpoxinfo::RPCPoxInfoData;
6061
use stacks::net::api::getsortition::SortitionInfo;
@@ -1010,6 +1011,18 @@ pub fn get_block(http_origin: &str, block_id: &StacksBlockId) -> Option<StacksBl
10101011
}
10111012
}
10121013

1014+
pub fn get_node_health(conf: &Config) -> RPCGetHealthResponse {
1015+
let http_origin = format!("http://{}", &conf.node.rpc_bind);
1016+
let client = reqwest::blocking::Client::new();
1017+
let path = format!("{http_origin}/v3/health");
1018+
client
1019+
.get(&path)
1020+
.send()
1021+
.unwrap()
1022+
.json::<RPCGetHealthResponse>()
1023+
.unwrap()
1024+
}
1025+
10131026
pub fn get_chain_info_result(conf: &Config) -> Result<RPCPeerInfoData, reqwest::Error> {
10141027
let http_origin = format!("http://{}", &conf.node.rpc_bind);
10151028
let client = reqwest::blocking::Client::new();

stackslib/src/net/api/gethealth.rs

Lines changed: 32 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,15 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
use std::fmt;
18-
use std::str::FromStr;
19-
2017
use regex::{Captures, Regex};
2118
use stacks_common::types::net::PeerHost;
22-
use stacks_common::types::StacksEpochId;
2319

24-
use crate::net::db::PeerDB;
2520
use crate::net::http::{
2621
parse_json, Error, HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse,
2722
HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
2823
};
2924
use crate::net::httpcore::{RPCRequestHandler, StacksHttpRequest, StacksHttpResponse};
30-
use crate::net::{
31-
infer_initial_burnchain_block_download, Error as NetError, NeighborAddress, StacksNodeState,
32-
};
25+
use crate::net::{Error as NetError, StacksNodeState};
3326

3427
/// The response for the GET /v3/health endpoint
3528
/// This endpoint returns the difference in height between the node and its most advanced neighbor
@@ -42,57 +35,19 @@ pub struct RPCGetHealthResponse {
4235
pub difference_from_max_peer: u64,
4336
/// the max height of the node's most advanced neighbor
4437
pub max_stacks_height_of_neighbors: u64,
38+
/// the address of the node's most advanced neighbor
39+
pub max_stacks_neighbor_address: Option<String>,
4540
/// the height of this node
4641
pub node_stacks_tip_height: u64,
4742
}
4843

49-
const NEIGHBORS_SCOPE_PARAM_NAME: &str = "neighbors";
50-
51-
#[derive(Clone, Debug, PartialEq)]
52-
pub enum NeighborsScope {
53-
Initial,
54-
All,
55-
}
56-
57-
impl FromStr for NeighborsScope {
58-
type Err = crate::net::http::Error;
59-
60-
fn from_str(s: &str) -> Result<Self, Self::Err> {
61-
match s {
62-
"initial" => Ok(NeighborsScope::Initial),
63-
"all" => Ok(NeighborsScope::All),
64-
_ => Err(crate::net::http::Error::Http(
65-
400,
66-
format!(
67-
"Invalid `neighbors` query parameter: `{}`, allowed values are `initial` or `all`",
68-
s
69-
),
70-
)),
71-
}
72-
}
73-
}
74-
75-
impl fmt::Display for NeighborsScope {
76-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77-
let s = match self {
78-
NeighborsScope::Initial => "initial",
79-
NeighborsScope::All => "all",
80-
};
81-
write!(f, "{s}")
82-
}
83-
}
84-
8544
#[derive(Clone)]
8645
/// Empty request handler for the GET /v3/health endpoint
87-
pub struct RPCGetHealthRequestHandler {
88-
neighbors_scope: Option<NeighborsScope>,
89-
}
46+
pub struct RPCGetHealthRequestHandler {}
9047

9148
impl RPCGetHealthRequestHandler {
9249
pub fn new() -> Self {
93-
Self {
94-
neighbors_scope: None,
95-
}
50+
Self {}
9651
}
9752
}
9853

@@ -125,12 +80,7 @@ impl HttpRequest for RPCGetHealthRequestHandler {
12580
));
12681
}
12782

128-
let req_contents = HttpRequestContents::new().query_string(query);
129-
if let Some(scope) = req_contents.get_query_arg(NEIGHBORS_SCOPE_PARAM_NAME) {
130-
self.neighbors_scope = Some(scope.parse()?);
131-
}
132-
133-
Ok(req_contents)
83+
Ok(HttpRequestContents::new().query_string(query))
13484
}
13585
}
13686

@@ -145,9 +95,7 @@ fn create_error_response(
14595

14696
impl RPCRequestHandler for RPCGetHealthRequestHandler {
14797
/// Reset internal state
148-
fn restart(&mut self) {
149-
self.neighbors_scope = None;
150-
}
98+
fn restart(&mut self) {}
15199

152100
/// Make the response
153101
fn try_handle_request(
@@ -156,97 +104,30 @@ impl RPCRequestHandler for RPCGetHealthRequestHandler {
156104
_contents: HttpRequestContents,
157105
node: &mut StacksNodeState,
158106
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
159-
let neighbors_scope = self
160-
.neighbors_scope
161-
.take()
162-
.unwrap_or(NeighborsScope::Initial);
163-
let use_all_neighbors = neighbors_scope == NeighborsScope::All;
164-
165-
node.with_node_state(|network, _sortdb, _chainstate, _mempool, _rpc_args| {
166-
let current_epoch = network.get_current_epoch();
167-
168-
let neighbors_arg = if use_all_neighbors {
169-
None
170-
} else {
171-
let initial_neighbors = PeerDB::get_valid_initial_neighbors(
172-
network.peerdb.conn(),
173-
network.local_peer.network_id,
174-
current_epoch.network_epoch,
175-
network.peer_version,
176-
network.chain_view.burn_block_height,
107+
let ((max_stacks_neighbor_address, max_stacks_height_of_neighbors), node_stacks_tip_height) =
108+
node.with_node_state(|network, _sortdb, _chainstate, _mempool, _rpc_args| {
109+
(
110+
network
111+
.highest_stacks_neighbor
112+
.map(|(addr, height)| (Some(addr.to_string()), height))
113+
.unwrap_or((None, 0)),
114+
network.stacks_tip.height,
177115
)
178-
.map_err(NetError::from)?;
179-
180-
if initial_neighbors.is_empty() {
181-
return create_error_response(
182-
&preamble,
183-
"No viable bootstrap peers found, unable to determine health",
184-
);
185-
}
186-
Some(initial_neighbors)
187-
};
188-
189-
let peer_max_stacks_height_opt = {
190-
if current_epoch.epoch_id < StacksEpochId::Epoch30 {
191-
// When the node enters Epoch 3.0, ibd is not accurate. In nakamoto it's always set to false.
192-
// See the implementation of `RunLoop::start` in `stacks-node/src/run_loop/nakamoto.rs`,
193-
// specifically the section and comment where `let ibd = false`, for details.
194-
let ibd = infer_initial_burnchain_block_download(
195-
&network.burnchain,
196-
network.burnchain_tip.block_height,
197-
network.chain_view.burn_block_height,
198-
);
199-
200-
// get max block height amongst bootstrap nodes
201-
match network.inv_state.as_ref() {
202-
Some(inv_state) => {
203-
inv_state.get_max_stacks_height_of_neighbors(neighbors_arg.as_deref(), ibd)
204-
}
205-
None => {
206-
return create_error_response(
207-
&preamble,
208-
"Peer inventory state (Epoch 2.x) not found, unable to determine health.",
209-
);
210-
}
211-
}
212-
} else {
213-
let neighbors_arg: Option<Vec<NeighborAddress>> = neighbors_arg.as_ref().map(|v| v.iter().map(NeighborAddress::from_neighbor).collect());
214-
match network.block_downloader_nakamoto.as_ref() {
215-
Some(block_downloader_nakamoto) => {
216-
block_downloader_nakamoto.get_max_stacks_height_of_neighbors(neighbors_arg.as_deref())
217-
}
218-
None => {
219-
return create_error_response(
220-
&preamble,
221-
"Nakamoto block downloader not found (Epoch 3.0+), unable to determine health.",
222-
);
223-
}
224-
}
225-
}
226-
};
227-
228-
match peer_max_stacks_height_opt {
229-
Some(max_stacks_height_of_neighbors) => {
230-
// There could be a edge case where our node is ahead of all peers.
231-
let node_stacks_tip_height = network.stacks_tip.height;
232-
let difference_from_max_peer =
233-
max_stacks_height_of_neighbors.saturating_sub(node_stacks_tip_height);
234-
235-
let preamble = HttpResponsePreamble::ok_json(&preamble);
236-
let data = RPCGetHealthResponse {
237-
difference_from_max_peer,
238-
max_stacks_height_of_neighbors,
239-
node_stacks_tip_height,
240-
};
241-
let body = HttpResponseContents::try_from_json(&data)?;
242-
Ok((preamble, body))
243-
}
244-
None => create_error_response(
245-
&preamble,
246-
"Couldn't obtain stats on any bootstrap peers, unable to determine health.",
247-
),
248-
}
249-
})
116+
});
117+
118+
// There could be a edge case where our node is ahead of all peers.
119+
let difference_from_max_peer =
120+
max_stacks_height_of_neighbors.saturating_sub(node_stacks_tip_height);
121+
122+
let preamble = HttpResponsePreamble::ok_json(&preamble);
123+
let data = RPCGetHealthResponse {
124+
difference_from_max_peer,
125+
max_stacks_height_of_neighbors,
126+
max_stacks_neighbor_address,
127+
node_stacks_tip_height,
128+
};
129+
let body = HttpResponseContents::try_from_json(&data)?;
130+
Ok((preamble, body))
250131
}
251132
}
252133

@@ -263,15 +144,12 @@ impl HttpResponse for RPCGetHealthRequestHandler {
263144
}
264145

265146
impl StacksHttpRequest {
266-
pub fn new_gethealth(host: PeerHost, neighbors_scope: NeighborsScope) -> StacksHttpRequest {
147+
pub fn new_gethealth(host: PeerHost) -> StacksHttpRequest {
267148
StacksHttpRequest::new_for_peer(
268149
host,
269150
"GET".into(),
270151
"/v3/health".into(),
271-
HttpRequestContents::new().query_arg(
272-
NEIGHBORS_SCOPE_PARAM_NAME.into(),
273-
neighbors_scope.to_string(),
274-
),
152+
HttpRequestContents::new(),
275153
)
276154
.expect("FATAL: failed to construct request from infallible data")
277155
}

0 commit comments

Comments
 (0)