Skip to main content

sui_node/
admin.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
35// Example commands:
36//
37// Set buffer stake for current epoch 2 to 1500 basis points:
38//
39//   $ curl -X POST 'http://127.0.0.1:1337/set-override-buffer-stake?buffer_bps=1500&epoch=2'
40//
41// Clear buffer stake override for current epoch 2, use
42// ProtocolConfig::buffer_stake_for_protocol_upgrade_bps:
43//
44//   $ curl -X POST 'http://127.0.0.1:1337/clear-override-buffer-stake?epoch=2'
45//
46// Vote to close epoch 2 early
47//
48//   $ curl -X POST 'http://127.0.0.1:1337/force-close-epoch?epoch=2'
49//
50// View current all capabilities from all authorities that have been received by this node:
51//
52//   $ curl 'http://127.0.0.1:1337/capabilities'
53//
54// View the node config (private keys will be masked):
55//
56//   $ curl 'http://127.0.0.1:1337/node-config'
57//
58// Set a time-limited tracing config. After the duration expires, tracing will be disabled
59// automatically.
60//
61//   $ curl -X POST 'http://127.0.0.1:1337/enable-tracing?filter=info&duration=10s'
62//
63// Reset tracing to the TRACE_FILTER env var.
64//
65//   $ curl -X POST 'http://127.0.0.1:1337/reset-tracing'
66//
67// Get the node's randomness partial signatures for round 123.
68//
69//  $ curl 'http://127.0.0.1:1337/randomness-partial-sigs?round=123'
70//
71// Inject a randomness partial signature from another node, bypassing validity checks.
72//
73//  $ curl 'http://127.0.0.1:1337/randomness-inject-partial-sigs?authority_name=hexencodedname&round=123&sigs=base64encodedsigs'
74//
75// Inject a full signature from another node, bypassing validity checks.
76//
77//  $ curl 'http://127.0.0.1:1337/randomness-inject-full-sig?round=123&sigs=base64encodedsig'
78//
79// Get the estimated cost of a transaction
80//
81//  $ curl 'http://127.0.0.1:1337/get-tx-cost?tx=<tx_digest>'
82// Reconfigure traffic control policy
83//
84//  $ curl 'http://127.0.0.1:1337/traffic-control?error_threshold=100&spam_threshold=100&dry_run=true'
85//
86// Update endpoint address(es) for a peer
87//
88//  $ curl -X POST 'http://127.0.0.1:1337/update-endpoint?endpoint_type=p2p&id=<hex_encoded_peer_id>&addresses=<multiaddr1>,<multiaddr2>'
89//  $ curl -X POST 'http://127.0.0.1:1337/update-endpoint?endpoint_type=consensus&id=<hex_encoded_network_pubkey>&addresses=<multiaddr1>,<multiaddr2>'
90//
91// Dump the address prober's latest results (full addresses + per-address outcomes) as JSON.
92//
93//  $ curl 'http://127.0.0.1:1337/address-prober-report'
94
95const 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    // These params change the filter, and reset it after the duration expires.
207    filter: Option<String>,
208    duration: Option<String>,
209
210    // Change the trace output file (if file output was enabled at program start)
211    trace_file: Option<String>,
212
213    // Change the tracing sample rate
214    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    // Duration is required if filter is set
253    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    // Note private keys will be masked
329    (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}