1use crate::SuiNode;
5use crate::db_shell::{handle_delete, handle_ls, handle_read};
6use axum::{
7 Router,
8 extract::{Query, State},
9 http::StatusCode,
10 routing::{delete, get, post},
11};
12use base64::Engine;
13use fastcrypto::encoding::{Encoding, Hex};
14use fastcrypto::traits::ToFromBytes;
15use humantime::parse_duration;
16use mysten_network::Multiaddr;
17use serde::Deserialize;
18use std::sync::Arc;
19use std::{
20 net::{IpAddr, Ipv4Addr, SocketAddr},
21 str::FromStr,
22};
23use sui_network::endpoint_manager::{AddressSource, EndpointId};
24use sui_types::{
25 base_types::{AuthorityName, ConciseableName},
26 crypto::{NetworkPublicKey, RandomnessPartialSignature, RandomnessRound, RandomnessSignature},
27 digests::TransactionDigest,
28 error::SuiErrorKind,
29 traffic_control::TrafficControlReconfigParams,
30};
31use telemetry_subscribers::TracingHandle;
32use tokio::sync::oneshot;
33use tracing::info;
34
35const NO_TRACING_HANDLE: &str = "tracing handle not available";
96const LOGGING_ROUTE: &str = "/logging";
97const TRACING_ROUTE: &str = "/enable-tracing";
98const TRACING_RESET_ROUTE: &str = "/reset-tracing";
99const SET_BUFFER_STAKE_ROUTE: &str = "/set-override-buffer-stake";
100const CLEAR_BUFFER_STAKE_ROUTE: &str = "/clear-override-buffer-stake";
101const FORCE_CLOSE_EPOCH: &str = "/force-close-epoch";
102const CAPABILITIES: &str = "/capabilities";
103const NODE_CONFIG: &str = "/node-config";
104const RANDOMNESS_PARTIAL_SIGS_ROUTE: &str = "/randomness-partial-sigs";
105const RANDOMNESS_INJECT_PARTIAL_SIGS_ROUTE: &str = "/randomness-inject-partial-sigs";
106const RANDOMNESS_INJECT_FULL_SIG_ROUTE: &str = "/randomness-inject-full-sig";
107const GET_TX_COST_ROUTE: &str = "/get-tx-cost";
108const DUMP_CONSENSUS_TX_COST_ESTIMATES_ROUTE: &str = "/dump-consensus-tx-cost-estimates";
109const TRAFFIC_CONTROL: &str = "/traffic-control";
110const UPDATE_ENDPOINT: &str = "/update-endpoint";
111const ADDRESS_PROBER_REPORT: &str = "/address-prober-report";
112const DB_SHELL_LS: &str = "/db-shell/ls";
113const DB_SHELL_READ: &str = "/db-shell/read";
114const DB_SHELL_DELETE: &str = "/db-shell/delete";
115const BROADCAST_TX_DENY_CONFIG: &str = "/broadcast-transaction-deny-config";
116const WITHDRAW_TX_DENY_CONFIG: &str = "/withdraw-transaction-deny-config";
117const TX_DENY_CONFIG: &str = "/transaction-deny-config";
118
119pub(crate) struct AppState {
120 pub(crate) node: Arc<SuiNode>,
121 pub(crate) tracing_handle: Option<TracingHandle>,
122}
123
124pub async fn run_admin_server(
125 node: Arc<SuiNode>,
126 port: u16,
127 tracing_handle: Option<TracingHandle>,
128) {
129 let filter = tracing_handle
130 .as_ref()
131 .and_then(|h| h.get_log().ok())
132 .unwrap_or_else(|| NO_TRACING_HANDLE.to_string());
133
134 let app_state = AppState {
135 node,
136 tracing_handle,
137 };
138
139 let app = Router::new()
140 .route(LOGGING_ROUTE, get(get_filter))
141 .route(CAPABILITIES, get(capabilities))
142 .route(NODE_CONFIG, get(node_config))
143 .route(LOGGING_ROUTE, post(set_filter))
144 .route(
145 SET_BUFFER_STAKE_ROUTE,
146 post(set_override_protocol_upgrade_buffer_stake),
147 )
148 .route(
149 CLEAR_BUFFER_STAKE_ROUTE,
150 post(clear_override_protocol_upgrade_buffer_stake),
151 )
152 .route(FORCE_CLOSE_EPOCH, post(force_close_epoch))
153 .route(TRACING_ROUTE, post(enable_tracing))
154 .route(TRACING_RESET_ROUTE, post(reset_tracing))
155 .route(RANDOMNESS_PARTIAL_SIGS_ROUTE, get(randomness_partial_sigs))
156 .route(
157 RANDOMNESS_INJECT_PARTIAL_SIGS_ROUTE,
158 post(randomness_inject_partial_sigs),
159 )
160 .route(
161 RANDOMNESS_INJECT_FULL_SIG_ROUTE,
162 post(randomness_inject_full_sig),
163 )
164 .route(GET_TX_COST_ROUTE, get(get_tx_cost))
165 .route(
166 DUMP_CONSENSUS_TX_COST_ESTIMATES_ROUTE,
167 get(dump_consensus_tx_cost_estimates),
168 )
169 .route(TRAFFIC_CONTROL, post(traffic_control))
170 .route(UPDATE_ENDPOINT, post(update_endpoint))
171 .route(ADDRESS_PROBER_REPORT, get(address_prober_report))
172 .route(DB_SHELL_LS, get(handle_ls))
173 .route(DB_SHELL_READ, get(handle_read))
174 .route(DB_SHELL_DELETE, delete(handle_delete))
175 .route(
176 BROADCAST_TX_DENY_CONFIG,
177 post(broadcast_transaction_deny_config),
178 )
179 .route(
180 WITHDRAW_TX_DENY_CONFIG,
181 post(withdraw_transaction_deny_config),
182 )
183 .route(TX_DENY_CONFIG, get(transaction_deny_config_dump))
184 .with_state(Arc::new(app_state));
185
186 let socket_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
187 info!(
188 filter =% filter,
189 address =% socket_address,
190 "starting admin server"
191 );
192
193 let listener = tokio::net::TcpListener::bind(&socket_address)
194 .await
195 .unwrap();
196 axum::serve(
197 listener,
198 app.into_make_service_with_connect_info::<SocketAddr>(),
199 )
200 .await
201 .unwrap();
202}
203
204#[derive(Deserialize)]
205struct EnableTracing {
206 filter: Option<String>,
208 duration: Option<String>,
209
210 trace_file: Option<String>,
212
213 sample_rate: Option<f64>,
215}
216
217async fn enable_tracing(
218 State(state): State<Arc<AppState>>,
219 query: Query<EnableTracing>,
220) -> (StatusCode, String) {
221 let Some(tracing_handle) = &state.tracing_handle else {
222 return (StatusCode::UNPROCESSABLE_ENTITY, NO_TRACING_HANDLE.into());
223 };
224
225 let Query(EnableTracing {
226 filter,
227 duration,
228 trace_file,
229 sample_rate,
230 }) = query;
231
232 let mut response = Vec::new();
233
234 if let Some(sample_rate) = sample_rate {
235 tracing_handle.update_sampling_rate(sample_rate);
236 response.push(format!("sample rate set to {:?}", sample_rate));
237 }
238
239 if let Some(trace_file) = trace_file {
240 if let Err(err) = tracing_handle.update_trace_file(&trace_file) {
241 response.push(format!("can't update trace file: {:?}", err));
242 return (StatusCode::BAD_REQUEST, response.join("\n"));
243 } else {
244 response.push(format!("trace file set to {:?}", trace_file));
245 }
246 }
247
248 let Some(filter) = filter else {
249 return (StatusCode::OK, response.join("\n"));
250 };
251
252 let Some(duration) = duration else {
254 response.push("can't update filter: missing duration".into());
255 return (StatusCode::BAD_REQUEST, response.join("\n"));
256 };
257
258 let Ok(duration) = parse_duration(&duration) else {
259 response.push("can't update filter: invalid duration".into());
260 return (StatusCode::BAD_REQUEST, response.join("\n"));
261 };
262
263 match tracing_handle.update_trace_filter(&filter, duration) {
264 Ok(()) => {
265 response.push(format!("filter set to {:?}", filter));
266 response.push(format!("filter will be reset after {:?}", duration));
267 (StatusCode::OK, response.join("\n"))
268 }
269 Err(err) => {
270 response.push(format!("can't update filter: {:?}", err));
271 (StatusCode::BAD_REQUEST, response.join("\n"))
272 }
273 }
274}
275
276async fn reset_tracing(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
277 let Some(tracing_handle) = &state.tracing_handle else {
278 return (StatusCode::UNPROCESSABLE_ENTITY, NO_TRACING_HANDLE.into());
279 };
280 tracing_handle.reset_trace();
281 (
282 StatusCode::OK,
283 "tracing filter reset to TRACE_FILTER env var".into(),
284 )
285}
286
287async fn get_filter(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
288 let Some(tracing_handle) = &state.tracing_handle else {
289 return (StatusCode::UNPROCESSABLE_ENTITY, NO_TRACING_HANDLE.into());
290 };
291 match tracing_handle.get_log() {
292 Ok(filter) => (StatusCode::OK, filter),
293 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
294 }
295}
296
297async fn set_filter(
298 State(state): State<Arc<AppState>>,
299 new_filter: String,
300) -> (StatusCode, String) {
301 let Some(tracing_handle) = &state.tracing_handle else {
302 return (StatusCode::UNPROCESSABLE_ENTITY, NO_TRACING_HANDLE.into());
303 };
304 match tracing_handle.update_log(&new_filter) {
305 Ok(()) => {
306 info!(filter =% new_filter, "Log filter updated");
307 (StatusCode::OK, "".into())
308 }
309 Err(err) => (StatusCode::BAD_REQUEST, err.to_string()),
310 }
311}
312
313async fn capabilities(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
314 let epoch_store = state.node.state().load_epoch_store_one_call_per_task();
315
316 let capabilities = epoch_store.get_capabilities_v2();
317 let mut output = String::new();
318 for capability in capabilities.unwrap_or_default() {
319 output.push_str(&format!("{:?}\n", capability));
320 }
321
322 (StatusCode::OK, output)
323}
324
325async fn node_config(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
326 let node_config = &state.node.config;
327
328 (StatusCode::OK, format!("{:#?}\n", node_config))
330}
331
332#[derive(Deserialize)]
333struct Epoch {
334 epoch: u64,
335}
336
337async fn clear_override_protocol_upgrade_buffer_stake(
338 State(state): State<Arc<AppState>>,
339 epoch: Query<Epoch>,
340) -> (StatusCode, String) {
341 let Query(Epoch { epoch }) = epoch;
342
343 match state
344 .node
345 .clear_override_protocol_upgrade_buffer_stake(epoch)
346 {
347 Ok(()) => (
348 StatusCode::OK,
349 "protocol upgrade buffer stake cleared\n".to_string(),
350 ),
351 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
352 }
353}
354
355#[derive(Deserialize)]
356struct SetBufferStake {
357 buffer_bps: u64,
358 epoch: u64,
359}
360
361async fn set_override_protocol_upgrade_buffer_stake(
362 State(state): State<Arc<AppState>>,
363 buffer_state: Query<SetBufferStake>,
364) -> (StatusCode, String) {
365 let Query(SetBufferStake { buffer_bps, epoch }) = buffer_state;
366
367 match state
368 .node
369 .set_override_protocol_upgrade_buffer_stake(epoch, buffer_bps)
370 {
371 Ok(()) => (
372 StatusCode::OK,
373 format!("protocol upgrade buffer stake set to '{}'\n", buffer_bps),
374 ),
375 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
376 }
377}
378
379async fn force_close_epoch(
380 State(state): State<Arc<AppState>>,
381 epoch: Query<Epoch>,
382) -> (StatusCode, String) {
383 let Query(Epoch {
384 epoch: expected_epoch,
385 }) = epoch;
386 let epoch_store = state.node.state().load_epoch_store_one_call_per_task();
387 let actual_epoch = epoch_store.epoch();
388 if actual_epoch != expected_epoch {
389 let err = SuiErrorKind::WrongEpoch {
390 expected_epoch,
391 actual_epoch,
392 };
393 return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
394 }
395
396 match state.node.close_epoch(&epoch_store).await {
397 Ok(()) => (
398 StatusCode::OK,
399 "close_epoch() called successfully\n".to_string(),
400 ),
401 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
402 }
403}
404
405#[derive(Deserialize)]
406struct Round {
407 round: u64,
408}
409
410async fn randomness_partial_sigs(
411 State(state): State<Arc<AppState>>,
412 round: Query<Round>,
413) -> (StatusCode, String) {
414 let Query(Round { round }) = round;
415
416 let (tx, rx) = oneshot::channel();
417 state
418 .node
419 .randomness_handle()
420 .admin_get_partial_signatures(RandomnessRound(round), tx);
421
422 let sigs = match rx.await {
423 Ok(sigs) => sigs,
424 Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
425 };
426
427 let output = format!(
428 "{}\n",
429 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(sigs)
430 );
431
432 (StatusCode::OK, output)
433}
434
435#[derive(Deserialize)]
436struct PartialSigsToInject {
437 hex_authority_name: String,
438 round: u64,
439 base64_sigs: String,
440}
441
442async fn randomness_inject_partial_sigs(
443 State(state): State<Arc<AppState>>,
444 args: Query<PartialSigsToInject>,
445) -> (StatusCode, String) {
446 let Query(PartialSigsToInject {
447 hex_authority_name,
448 round,
449 base64_sigs,
450 }) = args;
451
452 let authority_name = match AuthorityName::from_str(hex_authority_name.as_str()) {
453 Ok(authority_name) => authority_name,
454 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
455 };
456
457 let sigs: Vec<u8> = match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(base64_sigs) {
458 Ok(sigs) => sigs,
459 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
460 };
461
462 let sigs: Vec<RandomnessPartialSignature> = match bcs::from_bytes(&sigs) {
463 Ok(sigs) => sigs,
464 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
465 };
466
467 let (tx_result, rx_result) = oneshot::channel();
468 state
469 .node
470 .randomness_handle()
471 .admin_inject_partial_signatures(authority_name, RandomnessRound(round), sigs, tx_result);
472
473 match rx_result.await {
474 Ok(Ok(())) => (StatusCode::OK, "partial signatures injected\n".to_string()),
475 Ok(Err(e)) => (StatusCode::BAD_REQUEST, e.to_string()),
476 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
477 }
478}
479
480#[derive(Deserialize)]
481struct FullSigToInject {
482 round: u64,
483 base64_sig: String,
484}
485
486async fn randomness_inject_full_sig(
487 State(state): State<Arc<AppState>>,
488 args: Query<FullSigToInject>,
489) -> (StatusCode, String) {
490 let Query(FullSigToInject { round, base64_sig }) = args;
491
492 let sig: Vec<u8> = match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(base64_sig) {
493 Ok(sig) => sig,
494 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
495 };
496
497 let sig: RandomnessSignature = match bcs::from_bytes(&sig) {
498 Ok(sig) => sig,
499 Err(err) => return (StatusCode::BAD_REQUEST, err.to_string()),
500 };
501
502 let (tx_result, rx_result) = oneshot::channel();
503 state.node.randomness_handle().admin_inject_full_signature(
504 RandomnessRound(round),
505 sig,
506 tx_result,
507 );
508
509 match rx_result.await {
510 Ok(Ok(())) => (StatusCode::OK, "full signature injected\n".to_string()),
511 Ok(Err(e)) => (StatusCode::BAD_REQUEST, e.to_string()),
512 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
513 }
514}
515
516#[derive(Deserialize)]
517struct GetTxCost {
518 tx_digest: String,
519}
520
521async fn get_tx_cost(
522 State(state): State<Arc<AppState>>,
523 args: Query<GetTxCost>,
524) -> (StatusCode, String) {
525 let Query(GetTxCost { tx_digest }) = args;
526 let tx_digest = TransactionDigest::from_str(tx_digest.as_str()).unwrap();
527
528 let Some(transaction) = state
529 .node
530 .state()
531 .get_transaction_cache_reader()
532 .get_transaction_block(&tx_digest)
533 else {
534 return (StatusCode::BAD_REQUEST, "Transaction not found".to_string());
535 };
536
537 let Some(cost) = state
538 .node
539 .state()
540 .load_epoch_store_one_call_per_task()
541 .get_estimated_tx_cost(transaction.transaction_data())
542 .await
543 else {
544 return (StatusCode::BAD_REQUEST, "No estimate available".to_string());
545 };
546
547 (StatusCode::OK, cost.to_string())
548}
549
550async fn dump_consensus_tx_cost_estimates(
551 State(state): State<Arc<AppState>>,
552) -> (StatusCode, String) {
553 let epoch_store = state.node.state().load_epoch_store_one_call_per_task();
554 let estimates = epoch_store.get_consensus_tx_cost_estimates().await;
555 (StatusCode::OK, format!("{:#?}", estimates))
556}
557
558async fn traffic_control(
559 State(state): State<Arc<AppState>>,
560 args: Query<TrafficControlReconfigParams>,
561) -> (StatusCode, String) {
562 let Query(params) = args;
563 match state.node.state().reconfigure_traffic_control(params).await {
564 Ok(updated_state) => (
565 StatusCode::OK,
566 format!(
567 "Traffic control configured with:\n\
568 Error threshold: {:?}\n\
569 Spam threshold: {:?}\n\
570 Dry run: {:?}\n",
571 updated_state.error_threshold, updated_state.spam_threshold, updated_state.dry_run
572 ),
573 ),
574 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
575 }
576}
577
578#[derive(Deserialize)]
579struct UpdateEndpointArgs {
580 endpoint_type: String,
581 id: String,
582 addresses: String,
583}
584
585async fn update_endpoint(
586 State(state): State<Arc<AppState>>,
587 args: Query<UpdateEndpointArgs>,
588) -> (StatusCode, String) {
589 let Query(UpdateEndpointArgs {
590 endpoint_type,
591 id,
592 addresses,
593 }) = args;
594
595 let endpoint_id = match endpoint_type.as_str() {
596 "p2p" => {
597 let peer_id_bytes = match Hex::decode(&id) {
598 Ok(bytes) => bytes,
599 Err(err) => {
600 return (
601 StatusCode::BAD_REQUEST,
602 format!("Invalid id hex encoding: {err}"),
603 );
604 }
605 };
606
607 let peer_id_bytes: [u8; 32] = match peer_id_bytes.try_into() {
608 Ok(bytes) => bytes,
609 Err(_) => {
610 return (
611 StatusCode::BAD_REQUEST,
612 "p2p id must be 32 bytes".to_string(),
613 );
614 }
615 };
616
617 EndpointId::P2p(anemo::PeerId(peer_id_bytes))
618 }
619 "consensus" => {
620 let network_pubkey_bytes = match Hex::decode(&id) {
621 Ok(bytes) => bytes,
622 Err(err) => {
623 return (
624 StatusCode::BAD_REQUEST,
625 format!("Invalid id hex encoding: {err}"),
626 );
627 }
628 };
629
630 let network_pubkey = match NetworkPublicKey::from_bytes(&network_pubkey_bytes) {
631 Ok(key) => key,
632 Err(err) => {
633 return (
634 StatusCode::BAD_REQUEST,
635 format!("Invalid network public key: {err:?}"),
636 );
637 }
638 };
639
640 EndpointId::Consensus(network_pubkey)
641 }
642 _ => {
643 return (
644 StatusCode::BAD_REQUEST,
645 format!("Unknown endpoint_type: {endpoint_type}"),
646 );
647 }
648 };
649
650 let mut parsed_addresses = Vec::new();
651 for addr_str in addresses.split(',') {
652 let addr_str = addr_str.trim();
653 if addr_str.is_empty() {
654 continue;
655 }
656 match addr_str.parse::<Multiaddr>() {
657 Ok(addr) => parsed_addresses.push(addr),
658 Err(err) => {
659 return (
660 StatusCode::BAD_REQUEST,
661 format!("Invalid address '{addr_str}': {err}"),
662 );
663 }
664 }
665 }
666
667 if let Err(e) = state.node.endpoint_manager().update_endpoint(
668 endpoint_id,
669 AddressSource::Admin,
670 parsed_addresses.clone(),
671 ) {
672 return (StatusCode::BAD_REQUEST, e.to_string());
673 }
674
675 (
676 StatusCode::OK,
677 format!(
678 "Endpoint updated for {endpoint_type} endpoint {id} with {} address(es)\n",
679 parsed_addresses.len(),
680 ),
681 )
682}
683
684async fn submit_transaction_deny_config_update(
685 state: &Arc<AppState>,
686 rules: Option<sui_types::transaction_deny_rules::TransactionDenyRules>,
687) -> (StatusCode, String) {
688 let authority_state = state.node.state();
689 let epoch_store = authority_state.load_epoch_store_one_call_per_task();
690 if !epoch_store
691 .protocol_config()
692 .share_transaction_deny_config_in_consensus()
693 {
694 return (
695 StatusCode::PRECONDITION_FAILED,
696 "share_transaction_deny_config_in_consensus protocol flag is not enabled\n".to_string(),
697 );
698 }
699
700 let consensus_adapter = match state.node.consensus_adapter().await {
701 Some(adapter) => adapter,
702 None => {
703 return (
704 StatusCode::SERVICE_UNAVAILABLE,
705 "validator components not running; consensus adapter unavailable\n".to_string(),
706 );
707 }
708 };
709
710 match authority_state
711 .transaction_deny_config_manager()
712 .submit_broadcast(rules, &consensus_adapter, &epoch_store)
713 {
714 Ok(generation) => (
715 StatusCode::OK,
716 format!("UpdateTransactionDenyConfig submitted at generation {generation}\n"),
717 ),
718 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}\n")),
719 }
720}
721
722async fn broadcast_transaction_deny_config(
723 State(state): State<Arc<AppState>>,
724) -> (StatusCode, String) {
725 let local_rules = state
726 .node
727 .state()
728 .local_transaction_deny_config()
729 .rules()
730 .clone();
731 submit_transaction_deny_config_update(&state, Some(local_rules)).await
732}
733
734async fn withdraw_transaction_deny_config(
735 State(state): State<Arc<AppState>>,
736) -> (StatusCode, String) {
737 submit_transaction_deny_config_update(&state, None).await
738}
739
740async fn transaction_deny_config_dump(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
741 use serde_json::json;
742
743 let authority_state = state.node.state();
744 let manager = authority_state.transaction_deny_config_manager();
745 let local = manager.local();
746 let effective = manager.effective_config().load();
747 let peers = manager.peer_configs_snapshot();
748 let evaluation = manager.evaluate_status();
749
750 let peer_dump: serde_json::Map<String, serde_json::Value> = peers
751 .iter()
752 .map(|(authority, msg)| {
753 let key = format!("{}", authority.concise());
754 let value = json!({
755 "generation": msg.generation(),
756 "rules": msg.rules(),
757 });
758 (key, value)
759 })
760 .collect();
761
762 let prelisted_dump: Vec<serde_json::Value> = evaluation
763 .prelisted
764 .iter()
765 .map(|status| {
766 json!({
767 "name": status.name,
768 "stake_threshold_percent": status.stake_threshold_percent,
769 "eligible_stake": status.eligible_stake,
770 "voted_stake": status.voted_stake,
771 "voters": status
772 .voters
773 .iter()
774 .map(|v| format!("{}", v.concise()))
775 .collect::<Vec<_>>(),
776 "active": status.active,
777 })
778 })
779 .collect();
780
781 let defaults_dump: Vec<serde_json::Value> = evaluation
782 .defaults
783 .iter()
784 .map(|d| {
785 json!({
786 "name": d.name,
787 "element_kinds": d.element_kinds,
788 "stake_threshold_percent": d.stake_threshold_percent,
789 "eligible_stake": d.eligible_stake,
790 "applied_elements": d.applied_elements,
791 })
792 })
793 .collect();
794
795 let body = json!({
796 "local": {
797 "rules": local.rules(),
798 "has_dynamic_transaction_checks": local.has_dynamic_transaction_checks(),
799 },
800 "peers": peer_dump,
801 "effective": {
802 "rules": effective.rules(),
803 "has_dynamic_transaction_checks": effective.has_dynamic_transaction_checks(),
804 },
805 "voting": {
806 "prelisted": prelisted_dump,
807 "defaults": defaults_dump,
808 },
809 });
810
811 match serde_json::to_string_pretty(&body) {
812 Ok(s) => (StatusCode::OK, format!("{s}\n")),
813 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}\n")),
814 }
815}
816
817async fn address_prober_report(State(state): State<Arc<AppState>>) -> (StatusCode, String) {
818 let Some(report) = state.node.address_prober_report().await else {
819 return (
820 StatusCode::SERVICE_UNAVAILABLE,
821 "address prober is not running (node is not a validator, or the prober is disabled)\n"
822 .to_string(),
823 );
824 };
825 match serde_json::to_string_pretty(&report) {
826 Ok(json) => (StatusCode::OK, format!("{json}\n")),
827 Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
828 }
829}