1use std::{net::SocketAddr, str::FromStr, sync::Arc};
6
7use axum::{
8 Router,
9 extract::{Query, State},
10 http::StatusCode,
11 response::{IntoResponse as _, Response},
12 routing::{get, post},
13};
14use base64::Engine;
15use humantime::parse_duration;
16use iota_sdk_types::RandomnessRound;
17use iota_types::{
18 base_types::AuthorityName,
19 crypto::{RandomnessPartialSignature, RandomnessSignature},
20 error::IotaError,
21 traffic_control::TrafficControlReconfigParams,
22};
23use serde::Deserialize;
24use telemetry_subscribers::{TelemetryError, TracingHandle};
25use tokio::sync::oneshot;
26use tracing::info;
27
28use crate::IotaNode;
29
30const LOGGING_ROUTE: &str = "/logging";
105const TRACING_ROUTE: &str = "/enable-tracing";
106const TRACING_RESET_ROUTE: &str = "/reset-tracing";
107const SET_BUFFER_STAKE_ROUTE: &str = "/set-override-buffer-stake";
108const CLEAR_BUFFER_STAKE_ROUTE: &str = "/clear-override-buffer-stake";
109const FORCE_CLOSE_EPOCH: &str = "/force-close-epoch";
110const CAPABILITIES: &str = "/capabilities";
111const NODE_CONFIG: &str = "/node-config";
112const RANDOMNESS_PARTIAL_SIGS_ROUTE: &str = "/randomness-partial-sigs";
113const RANDOMNESS_INJECT_PARTIAL_SIGS_ROUTE: &str = "/randomness-inject-partial-sigs";
114const RANDOMNESS_INJECT_FULL_SIG_ROUTE: &str = "/randomness-inject-full-sig";
115const FLAMEGRAPH_ROUTE: &str = "/flamegraph";
116const TRAFFIC_CONTROL: &str = "/traffic-control";
117const METRICS_FILTER_ROUTE: &str = "/metrics/filters";
118const METRICS_FILTER_RESET_ROUTE: &str = "/metrics/filters/reset";
119
120struct AppState {
121 node: Arc<IotaNode>,
122 tracing_handle: TracingHandle,
123}
124
125pub async fn run_admin_server(
126 node: Arc<IotaNode>,
127 socket_address: SocketAddr,
128 tracing_handle: TracingHandle,
129) {
130 let filter = tracing_handle.get_log().unwrap();
131
132 let app_state = AppState {
133 node,
134 tracing_handle,
135 };
136
137 let app = Router::new()
138 .route(LOGGING_ROUTE, get(get_filter))
139 .route(CAPABILITIES, get(capabilities))
140 .route(NODE_CONFIG, get(node_config))
141 .route(LOGGING_ROUTE, post(set_filter))
142 .route(
143 SET_BUFFER_STAKE_ROUTE,
144 post(set_override_protocol_upgrade_buffer_stake),
145 )
146 .route(
147 CLEAR_BUFFER_STAKE_ROUTE,
148 post(clear_override_protocol_upgrade_buffer_stake),
149 )
150 .route(FORCE_CLOSE_EPOCH, post(force_close_epoch))
151 .route(TRACING_ROUTE, post(enable_tracing))
152 .route(TRACING_RESET_ROUTE, post(reset_tracing))
153 .route(RANDOMNESS_PARTIAL_SIGS_ROUTE, get(randomness_partial_sigs))
154 .route(
155 RANDOMNESS_INJECT_PARTIAL_SIGS_ROUTE,
156 post(randomness_inject_partial_sigs),
157 )
158 .route(
159 RANDOMNESS_INJECT_FULL_SIG_ROUTE,
160 post(randomness_inject_full_sig),
161 )
162 .route(FLAMEGRAPH_ROUTE, get(flamegraph))
163 .route(TRAFFIC_CONTROL, post(traffic_control))
164 .route(METRICS_FILTER_ROUTE, get(get_metrics_filter))
165 .route(METRICS_FILTER_ROUTE, post(set_metrics_filter))
166 .route(METRICS_FILTER_RESET_ROUTE, post(reset_metrics_filter))
167 .with_state(Arc::new(app_state));
168
169 info!(
170 filter =% filter,
171 address =% socket_address,
172 "starting admin server"
173 );
174
175 let listener = tokio::net::TcpListener::bind(&socket_address)
176 .await
177 .unwrap();
178 axum::serve(
179 listener,
180 app.into_make_service_with_connect_info::<SocketAddr>(),
181 )
182 .await
183 .unwrap();
184}
185
186#[derive(Deserialize)]
187struct EnableTracing {
188 filter: Option<String>,
190 duration: Option<String>,
191
192 trace_file: Option<String>,
194
195 sample_rate: Option<f64>,
197}
198
199async fn enable_tracing(
200 State(state): State<Arc<AppState>>,
201 query: Query<EnableTracing>,
202) -> (StatusCode, String) {
203 let Query(EnableTracing {
204 filter,
205 duration,
206 trace_file,
207 sample_rate,
208 }) = query;
209
210 let mut response = Vec::new();
211
212 if let Some(sample_rate) = sample_rate {
213 state.tracing_handle.update_sampling_rate(sample_rate);
214 response.push(format!("sample rate set to {sample_rate:?}"));
215 }
216
217 if let Some(trace_file) = trace_file {
218 if let Err(err) = state.tracing_handle.update_trace_file(&trace_file) {
219 response.push(format!("can't update trace file: {err:?}"));
220 return (StatusCode::BAD_REQUEST, response.join("\n"));
221 } else {
222 response.push(format!("trace file set to {trace_file:?}"));
223 }
224 }
225
226 let Some(filter) = filter else {
227 return (StatusCode::OK, response.join("\n"));
228 };
229
230 let Some(duration) = duration else {
232 response.push("can't update filter: missing duration".into());
233 return (StatusCode::BAD_REQUEST, response.join("\n"));
234 };
235
236 let Ok(duration) = parse_duration(&duration) else {
237 response.push("can't update filter: invalid duration".into());
238 return (StatusCode::BAD_REQUEST, response.join("\n"));
239 };
240
241 match state.tracing_handle.update_trace_filter(&filter, duration) {
242 Ok(()) => {
243 response.push(format!("filter set to {filter:?}"));
244 response.push(format!("filter will be reset after {duration:?}"));
245 (StatusCode::OK, response.join("\n"))
246 }
247 Err(TelemetryError::TracingDisabled) => {
248 response.push("can't update filter: tracing is not enabled. to enable it, run the node with 'TRACE_FILTER' set.".into());
249 (StatusCode::NOT_IMPLEMENTED, response.join("\n"))
250 }
251 Err(err) => {
252 response.push(format!("can't update filter: {err:?}"));
253 (StatusCode::BAD_REQUEST, response.join("\n"))
254 }
255 }
256}
257
258async fn reset_tracing(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
259 match state.tracing_handle.reset_trace() {
260 Ok(()) => (
261 StatusCode::OK,
262 "tracing filter reset to TRACE_FILTER env var".into(),
263 ),
264 Err(TelemetryError::TracingDisabled) => (
265 StatusCode::NOT_IMPLEMENTED,
266 "tracing is not enabled. to enable it, run the node with 'TRACE_FILTER' set.".into(),
267 ),
268 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
269 }
270}
271
272async fn get_filter(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
273 match state.tracing_handle.get_log() {
274 Ok(filter) => (StatusCode::OK, filter),
275 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
276 }
277}
278
279async fn set_filter(
280 State(state): State<Arc<AppState>>,
281 new_filter: String,
282) -> (StatusCode, String) {
283 match state.tracing_handle.update_log(&new_filter) {
284 Ok(()) => {
285 info!(filter =% new_filter, "Log filter updated");
286 (StatusCode::OK, "".into())
287 }
288 Err(err) => (StatusCode::BAD_REQUEST, err.to_string()),
289 }
290}
291
292async fn capabilities(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
293 let epoch_store = state.node.state().load_epoch_store_one_call_per_task();
294
295 let mut output = String::new();
296 let capabilities = epoch_store.get_capabilities_v1();
297 for capability in capabilities.unwrap_or_default() {
298 output.push_str(&format!("{capability:?}\n"));
299 }
300
301 (StatusCode::OK, output)
302}
303
304async fn node_config(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
305 let node_config = &state.node.config;
306
307 (StatusCode::OK, format!("{node_config:#?}\n"))
309}
310
311#[derive(Deserialize)]
312struct Epoch {
313 epoch: u64,
314}
315
316async fn clear_override_protocol_upgrade_buffer_stake(
317 State(state): State<Arc<AppState>>,
318 epoch: Query<Epoch>,
319) -> (StatusCode, String) {
320 let Query(Epoch { epoch }) = epoch;
321
322 match state
323 .node
324 .clear_override_protocol_upgrade_buffer_stake(epoch)
325 {
326 Ok(()) => (
327 StatusCode::OK,
328 "protocol upgrade buffer stake cleared\n".to_string(),
329 ),
330 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
331 }
332}
333
334#[derive(Deserialize)]
335struct SetBufferStake {
336 buffer_bps: u64,
337 epoch: u64,
338}
339
340async fn set_override_protocol_upgrade_buffer_stake(
341 State(state): State<Arc<AppState>>,
342 buffer_state: Query<SetBufferStake>,
343) -> (StatusCode, String) {
344 let Query(SetBufferStake { buffer_bps, epoch }) = buffer_state;
345
346 match state
347 .node
348 .set_override_protocol_upgrade_buffer_stake(epoch, buffer_bps)
349 {
350 Ok(()) => (
351 StatusCode::OK,
352 format!("protocol upgrade buffer stake set to '{buffer_bps}'\n"),
353 ),
354 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
355 }
356}
357
358async fn force_close_epoch(
359 State(state): State<Arc<AppState>>,
360 epoch: Query<Epoch>,
361) -> (StatusCode, String) {
362 let Query(Epoch {
363 epoch: expected_epoch,
364 }) = epoch;
365 let epoch_store = state.node.state().load_epoch_store_one_call_per_task();
366 let actual_epoch = epoch_store.epoch();
367 if actual_epoch != expected_epoch {
368 let err = IotaError::WrongEpoch {
369 expected_epoch,
370 actual_epoch,
371 };
372 return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
373 }
374
375 match state.node.close_epoch(&epoch_store).await {
376 Ok(()) => (
377 StatusCode::OK,
378 "close_epoch() called successfully\n".to_string(),
379 ),
380 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
381 }
382}
383
384#[derive(Deserialize)]
385struct Round {
386 round: u64,
387}
388
389async fn randomness_partial_sigs(
390 State(state): State<Arc<AppState>>,
391 round: Query<Round>,
392) -> (StatusCode, String) {
393 let Query(Round { round }) = round;
394
395 let (tx, rx) = oneshot::channel();
396 state
397 .node
398 .randomness_handle()
399 .admin_get_partial_signatures(RandomnessRound::new(round), tx);
400
401 let sigs = match rx.await {
402 Ok(sigs) => sigs,
403 Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
404 };
405
406 let output = format!(
407 "{}\n",
408 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(sigs)
409 );
410
411 (StatusCode::OK, output)
412}
413
414#[derive(Deserialize)]
415struct PartialSigsToInject {
416 hex_authority_name: String,
417 round: u64,
418 base64_sigs: String,
419}
420
421async fn randomness_inject_partial_sigs(
422 State(state): State<Arc<AppState>>,
423 args: Query<PartialSigsToInject>,
424) -> (StatusCode, String) {
425 let Query(PartialSigsToInject {
426 hex_authority_name,
427 round,
428 base64_sigs,
429 }) = args;
430
431 let authority_name = match AuthorityName::from_str(hex_authority_name.as_str()) {
432 Ok(authority_name) => authority_name,
433 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
434 };
435
436 let sigs: Vec<u8> = match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(base64_sigs) {
437 Ok(sigs) => sigs,
438 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
439 };
440
441 let sigs: Vec<RandomnessPartialSignature> = match bcs::from_bytes(&sigs) {
442 Ok(sigs) => sigs,
443 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
444 };
445
446 let (tx_result, rx_result) = oneshot::channel();
447 state
448 .node
449 .randomness_handle()
450 .admin_inject_partial_signatures(
451 authority_name,
452 RandomnessRound::new(round),
453 sigs,
454 tx_result,
455 );
456
457 match rx_result.await {
458 Ok(Ok(())) => (StatusCode::OK, "partial signatures injected\n".to_string()),
459 Ok(Err(e)) => (StatusCode::BAD_REQUEST, e.to_string()),
460 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
461 }
462}
463
464#[derive(Deserialize)]
465struct FullSigToInject {
466 round: u64,
467 base64_sig: String,
468}
469
470async fn randomness_inject_full_sig(
471 State(state): State<Arc<AppState>>,
472 args: Query<FullSigToInject>,
473) -> (StatusCode, String) {
474 let Query(FullSigToInject { round, base64_sig }) = args;
475
476 let sig: Vec<u8> = match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(base64_sig) {
477 Ok(sig) => sig,
478 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
479 };
480
481 let sig: RandomnessSignature = match bcs::from_bytes(&sig) {
482 Ok(sig) => sig,
483 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
484 };
485
486 let (tx_result, rx_result) = oneshot::channel();
487 state.node.randomness_handle().admin_inject_full_signature(
488 RandomnessRound::new(round),
489 sig,
490 tx_result,
491 );
492
493 match rx_result.await {
494 Ok(Ok(())) => (StatusCode::OK, "full signature injected\n".to_string()),
495 Ok(Err(e)) => (StatusCode::BAD_REQUEST, e.to_string()),
496 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
497 }
498}
499
500#[derive(Deserialize)]
501struct Flamegraph {
502 #[serde(default)]
504 svg: bool,
505 #[serde(default)]
507 width: usize,
508 #[serde(default)]
510 running: bool,
511 #[serde(default)]
513 completed: bool,
514 #[serde(default)]
516 graph_id: String,
517 #[serde(default)]
519 mem: bool,
520}
521
522async fn flamegraph(State(state): State<Arc<AppState>>, query: Query<Flamegraph>) -> Response {
523 if let Some(sub) = state.tracing_handle.get_flamegraph() {
524 let Query(Flamegraph {
525 svg,
526 width,
527 mut running,
528 mut completed,
529 graph_id,
530 mem,
531 }) = query;
532 if !running && !completed {
533 running = true;
534 completed = true;
535 }
536 if svg {
537 #[cfg(not(all(feature = "flamegraph-alloc", nightly)))]
538 {
539 if mem {
540 return (
541 StatusCode::BAD_REQUEST,
542 "memory flamegraphs are not supported (re-run iota-node with 'flamegraph-alloc' feature enabled and on nightly Rust toolchain)",
543 )
544 .into_response();
545 }
546 }
547
548 let width = if width == 0 { Some(1920) } else { Some(width) };
550 let config = telemetry_subscribers::flamegraph::SvgConfig {
551 width,
552 #[cfg(all(feature = "flamegraph-alloc", nightly))]
553 measure_mem: mem,
554 ..Default::default()
555 };
556 let svg = if !graph_id.is_empty() {
557 sub.get_svg(&graph_id, running, completed, &config)
558 } else {
559 sub.get_combined_svg("iota-node", running, completed, &config)
560 };
561 if let Some(svg) = svg {
562 (
563 [(
564 axum::http::header::CONTENT_TYPE,
565 axum::http::header::HeaderValue::from_static("image/svg+xml"),
566 )],
567 svg.into_string(),
568 )
569 .into_response()
570 } else {
571 (StatusCode::NOT_FOUND, "Flamegraphs not found\n").into_response()
572 }
573 } else {
574 let nested_frames = if !graph_id.is_empty() {
576 sub.get_nested_set(&graph_id, running, completed)
577 } else {
578 sub.get_nested_sets("iota-node", running, completed)
579 };
580 if !nested_frames.is_empty() {
581 axum::Json(nested_frames).into_response()
582 } else {
583 (StatusCode::NOT_FOUND, "Flamegraphs not found\n").into_response()
584 }
585 }
586 } else {
587 (
588 StatusCode::NOT_FOUND,
589 "Flamegraphs are not enabled (re-run iota-node with TRACE_FLAMEGRAPH=1)\n",
590 )
591 .into_response()
592 }
593}
594
595async fn traffic_control(
596 State(state): State<Arc<AppState>>,
597 args: Query<TrafficControlReconfigParams>,
598) -> (StatusCode, String) {
599 let Query(params) = args;
600 match state.node.state().reconfigure_traffic_control(params).await {
601 Ok(updated_state) => (
602 StatusCode::OK,
603 format!(
604 "Traffic control configured with:\n\
605 Error threshold: {:?}\n\
606 Spam threshold: {:?}\n\
607 Dry run: {:?}\n",
608 updated_state.error_threshold, updated_state.spam_threshold, updated_state.dry_run
609 ),
610 ),
611 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
612 }
613}
614
615async fn get_metrics_filter(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
616 let filter = state.node.registry_service().filter();
617 (
618 StatusCode::OK,
619 format!(
620 "metrics exposure filter:\n\
621 current: {}\n\n\n\
622 (startup: {})\n",
623 filter.filter_string(),
624 filter.startup_filter_string(),
625 ),
626 )
627}
628
629#[derive(Deserialize)]
630struct MetricsFilterUpdate {
631 filter: String,
632}
633
634async fn set_metrics_filter(
635 State(state): State<Arc<AppState>>,
636 Query(MetricsFilterUpdate { filter }): Query<MetricsFilterUpdate>,
637) -> (StatusCode, String) {
638 let new_filter = filter.trim();
639 match state.node.registry_service().set_runtime_filter(new_filter) {
640 Ok(()) => {
641 info!(filter =% new_filter, "Metrics filter updated");
642 (
643 StatusCode::OK,
644 format!("metrics filter set to {new_filter:?}\n"),
645 )
646 }
647 Err(err) => (StatusCode::BAD_REQUEST, format!("{err}\n")),
648 }
649}
650
651async fn reset_metrics_filter(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
652 state.node.registry_service().reset_runtime_filter();
653 info!("Metrics filter reset to startup configuration");
654 (
655 StatusCode::OK,
656 "metrics filter reset to startup configuration\n".into(),
657 )
658}