Skip to content

Commit 32e74fb

Browse files
committed
chore: Add /v3/signer/ endpoint
1 parent c454b35 commit 32e74fb

File tree

2 files changed

+269
-0
lines changed

2 files changed

+269
-0
lines changed

stackslib/src/net/api/getsigner.rs

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright (C) 2024 Stacks Open Internet Foundation
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
use clarity::util::secp256k1::Secp256k1PublicKey;
16+
use regex::{Captures, Regex};
17+
use serde_json::json;
18+
use stacks_common::types::chainstate::StacksBlockId;
19+
use stacks_common::types::net::PeerHost;
20+
use stacks_common::util::hash::Sha256Sum;
21+
22+
use crate::burnchains::Burnchain;
23+
use crate::chainstate::burn::db::sortdb::SortitionDB;
24+
use crate::chainstate::coordinator::OnChainRewardSetProvider;
25+
use crate::chainstate::stacks::boot::{
26+
PoxVersions, RewardSet, POX_1_NAME, POX_2_NAME, POX_3_NAME, POX_4_NAME,
27+
};
28+
use crate::chainstate::stacks::db::StacksChainState;
29+
use crate::chainstate::stacks::Error as ChainError;
30+
use crate::core::mempool::MemPoolDB;
31+
use crate::net::http::{
32+
parse_json, Error, HttpBadRequest, HttpNotFound, HttpRequest, HttpRequestContents,
33+
HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload,
34+
HttpResponsePreamble, HttpServerError,
35+
};
36+
use crate::net::httpcore::{
37+
HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp,
38+
StacksHttpRequest, StacksHttpResponse,
39+
};
40+
use crate::net::p2p::PeerNetwork;
41+
use crate::net::{Error as NetError, StacksNodeState, TipRequest};
42+
use crate::util_lib::boot::boot_code_id;
43+
use crate::util_lib::db::Error as DBError;
44+
45+
#[derive(Clone, Default)]
46+
pub struct GetSignerRequestHandler {
47+
signer_pubkey: Option<Secp256k1PublicKey>,
48+
reward_cycle: Option<u64>,
49+
}
50+
51+
#[derive(Debug, Serialize, Deserialize)]
52+
pub struct GetSignerResponse {
53+
pub blocks_signed: u64,
54+
}
55+
56+
pub enum GetSignerErrors {
57+
NotAvailableYet(crate::chainstate::coordinator::Error),
58+
Other(String),
59+
}
60+
61+
impl GetSignerErrors {
62+
pub const NOT_AVAILABLE_ERR_TYPE: &'static str = "not_available_try_again";
63+
pub const OTHER_ERR_TYPE: &'static str = "other";
64+
65+
pub fn error_type_string(&self) -> &'static str {
66+
match self {
67+
Self::NotAvailableYet(_) => Self::NOT_AVAILABLE_ERR_TYPE,
68+
Self::Other(_) => Self::OTHER_ERR_TYPE,
69+
}
70+
}
71+
}
72+
73+
impl From<&str> for GetSignerErrors {
74+
fn from(value: &str) -> Self {
75+
GetSignerErrors::Other(value.into())
76+
}
77+
}
78+
79+
impl std::fmt::Display for GetSignerErrors {
80+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81+
match self {
82+
GetSignerErrors::NotAvailableYet(e) => write!(f, "Could not read reward set. Prepare phase may not have started for this cycle yet. Err = {e:?}"),
83+
GetSignerErrors::Other(msg) => write!(f, "{msg}")
84+
}
85+
}
86+
}
87+
88+
/// Decode the HTTP request
89+
impl HttpRequest for GetSignerRequestHandler {
90+
fn verb(&self) -> &'static str {
91+
"GET"
92+
}
93+
94+
fn path_regex(&self) -> Regex {
95+
Regex::new(
96+
r#"^/v3/stacker_set/(?P<signer_pubkey>[0-9a-f]{66})/(?P<cycle_num>[0-9]{1,10})$"#,
97+
)
98+
.unwrap()
99+
}
100+
101+
fn metrics_identifier(&self) -> &str {
102+
"/v3/signer/:signer_pubkey/:cycle_num"
103+
}
104+
105+
/// Try to decode this request.
106+
/// There's nothing to load here, so just make sure the request is well-formed.
107+
fn try_parse_request(
108+
&mut self,
109+
preamble: &HttpRequestPreamble,
110+
captures: &Captures,
111+
query: Option<&str>,
112+
_body: &[u8],
113+
) -> Result<HttpRequestContents, Error> {
114+
if preamble.get_content_length() != 0 {
115+
return Err(Error::DecodeError(
116+
"Invalid Http request: expected 0-length body".into(),
117+
));
118+
}
119+
120+
let Some(cycle_num_str) = captures.name("cycle_num") else {
121+
return Err(Error::DecodeError(
122+
"Missing in request path: `cycle_num`".into(),
123+
));
124+
};
125+
let Some(signer_pubkey_str) = captures.name("signer_pubkey") else {
126+
return Err(Error::DecodeError(
127+
"Missing in request path: `signer_pubkey`".into(),
128+
));
129+
};
130+
131+
let cycle_num = u64::from_str_radix(cycle_num_str.into(), 10)
132+
.map_err(|e| Error::DecodeError(format!("Failed to parse cycle number: {e}")))?;
133+
134+
let signer_pubkey = Secp256k1PublicKey::from_hex(signer_pubkey_str.into())
135+
.map_err(|e| Error::DecodeError(format!("Failed to signer public key: {e}")))?;
136+
137+
self.signer_pubkey = Some(signer_pubkey);
138+
self.reward_cycle = Some(cycle_num);
139+
140+
Ok(HttpRequestContents::new().query_string(query))
141+
}
142+
}
143+
144+
impl RPCRequestHandler for GetSignerRequestHandler {
145+
/// Reset internal state
146+
fn restart(&mut self) {
147+
self.signer_pubkey = None;
148+
self.reward_cycle = None;
149+
}
150+
151+
/// Make the response
152+
fn try_handle_request(
153+
&mut self,
154+
preamble: HttpRequestPreamble,
155+
contents: HttpRequestContents,
156+
node: &mut StacksNodeState,
157+
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
158+
let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
159+
Ok(tip) => tip,
160+
Err(error_resp) => {
161+
return error_resp.try_into_contents().map_err(NetError::from);
162+
}
163+
};
164+
165+
let signer_pubkey = self
166+
.signer_pubkey
167+
.take()
168+
.ok_or(NetError::SendError("Missing `signer_pubkey`".into()))?;
169+
170+
let reward_cycle = self
171+
.reward_cycle
172+
.take()
173+
.ok_or(NetError::SendError("Missing `reward_cycle`".into()))?;
174+
175+
let result = node.with_node_state(|_network, _sortdb, _chainstate, _mempool, _rpc_args| {
176+
// TODO
177+
if true {
178+
Ok(0u64)
179+
} else {
180+
Err("Something went wrong")
181+
}
182+
});
183+
184+
let response = match result {
185+
Ok(response) => response,
186+
Err(error) => {
187+
return StacksHttpResponse::new_error(
188+
&preamble,
189+
&HttpNotFound::new(error.to_string()),
190+
)
191+
.try_into_contents()
192+
.map_err(NetError::from);
193+
}
194+
};
195+
196+
let mut preamble = HttpResponsePreamble::ok_json(&preamble);
197+
preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height()));
198+
let body = HttpResponseContents::try_from_json(&response)?;
199+
Ok((preamble, body))
200+
}
201+
}
202+
203+
impl HttpResponse for GetSignerRequestHandler {
204+
fn try_parse_response(
205+
&self,
206+
preamble: &HttpResponsePreamble,
207+
body: &[u8],
208+
) -> Result<HttpResponsePayload, Error> {
209+
let response: GetSignerResponse = parse_json(preamble, body)?;
210+
Ok(HttpResponsePayload::try_from_json(response)?)
211+
}
212+
}
213+
214+
impl StacksHttpRequest {
215+
/// Make a new getinfo request to this endpoint
216+
pub fn new_getsigner(
217+
host: PeerHost,
218+
signer_pubkey: &Secp256k1PublicKey,
219+
cycle_num: u64,
220+
tip_req: TipRequest,
221+
) -> StacksHttpRequest {
222+
StacksHttpRequest::new_for_peer(
223+
host,
224+
"GET".into(),
225+
format!("/v3/signer/{}/{cycle_num}", signer_pubkey.to_hex()),
226+
HttpRequestContents::new().for_tip(tip_req),
227+
)
228+
.expect("FATAL: failed to construct request from infallible data")
229+
}
230+
}
231+
232+
impl StacksHttpResponse {
233+
pub fn decode_signer(self) -> Result<GetSignerResponse, NetError> {
234+
let contents = self.get_http_payload_ok()?;
235+
let response_json: serde_json::Value = contents.try_into()?;
236+
let response: GetSignerResponse = serde_json::from_value(response_json)
237+
.map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
238+
Ok(response)
239+
}
240+
}
241+
242+
#[cfg(test)]
243+
mod test {
244+
use super::GetSignerErrors;
245+
246+
#[test]
247+
// Test the formatting and error type strings of GetSignerErrors
248+
fn get_signer_errors() {
249+
let not_available_err = GetSignerErrors::NotAvailableYet(
250+
crate::chainstate::coordinator::Error::PoXNotProcessedYet,
251+
);
252+
let other_err = GetSignerErrors::Other("foo".into());
253+
254+
assert_eq!(
255+
not_available_err.error_type_string(),
256+
GetSignerErrors::NOT_AVAILABLE_ERR_TYPE
257+
);
258+
assert_eq!(
259+
other_err.error_type_string(),
260+
GetSignerErrors::OTHER_ERR_TYPE
261+
);
262+
263+
assert!(not_available_err
264+
.to_string()
265+
.starts_with("Could not read reward set"));
266+
assert_eq!(other_err.to_string(), "foo".to_string());
267+
}
268+
}

stackslib/src/net/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub mod getmicroblocks_indexed;
5555
pub mod getmicroblocks_unconfirmed;
5656
pub mod getneighbors;
5757
pub mod getpoxinfo;
58+
pub mod getsigner;
5859
pub mod getsortition;
5960
pub mod getstackerdbchunk;
6061
pub mod getstackerdbmetadata;

0 commit comments

Comments
 (0)