Skip to main content

fedimint_server/net/api/
pkarr_publish.rs

1use std::time::Duration;
2
3use fedimint_core::db::Database;
4use fedimint_core::envs::{
5    FM_PKARR_DHT_ENABLE_ENV, FM_PKARR_ENABLE_ENV, FM_PKARR_RELAYS_ENABLE_ENV, is_env_var_set,
6};
7use fedimint_core::secp256k1::SecretKey;
8use fedimint_core::task::{TaskGroup, sleep};
9use fedimint_core::util::FmtCompact;
10use fedimint_derive_secret::{ChildId, DerivableSecret};
11use fedimint_logging::LOG_NET_API;
12use pkarr::SignedPacket;
13use tracing::{debug, info, warn};
14
15use crate::config::ServerConfig;
16
17/// Child key index for deriving the pkarr identity from the broadcast secret
18const PKARR_IDENTITY_CHILD_ID: ChildId = ChildId(0);
19
20const PUBLISH_INTERVAL_SECS: u64 = 600;
21const FAILURE_RETRY_SECS: u64 = 60;
22const INITIAL_DELAY_SECS: u64 = 10;
23const TXT_RECORD_TTL: u32 = 1800;
24
25/// Derive a pkarr keypair deterministically from the server's broadcast secret
26/// key.
27///
28/// Uses HKDF-based derivation with domain separation to produce an ed25519
29/// seed.
30pub fn derive_pkarr_keypair(broadcast_sk: &SecretKey) -> pkarr::Keypair {
31    let root = DerivableSecret::new_root(&broadcast_sk.secret_bytes(), b"fedimint-pkarr");
32    let pkarr_child = root.child_key(PKARR_IDENTITY_CHILD_ID);
33    let seed: [u8; 32] = pkarr_child.to_random_bytes();
34    pkarr::Keypair::from_secret_key(&seed)
35}
36
37/// Get the z-base32 encoded pkarr public key derived from the broadcast secret
38/// key.
39pub fn pkarr_id_z32(broadcast_sk: &SecretKey) -> String {
40    derive_pkarr_keypair(broadcast_sk).to_z32()
41}
42
43/// Spawn a background task that periodically publishes this guardian's API
44/// URL(s) as pkarr DNS TXT records.
45pub async fn start_pkarr_publish_service(
46    db: &Database,
47    tg: &TaskGroup,
48    cfg: &ServerConfig,
49) -> anyhow::Result<()> {
50    let keypair = derive_pkarr_keypair(&cfg.private.broadcast_secret_key);
51
52    let pkarr_enabled =
53        fedimint_core::envs::is_env_var_set_opt(FM_PKARR_ENABLE_ENV).unwrap_or(true);
54
55    if !pkarr_enabled {
56        info!(
57            target: LOG_NET_API,
58            pkarr_id = %keypair.to_z32(),
59            "Pkarr publishing disabled via {FM_PKARR_ENABLE_ENV}"
60        );
61        return Ok(());
62    }
63
64    let dht_enabled = is_env_var_set(FM_PKARR_DHT_ENABLE_ENV);
65    let relays_enabled =
66        fedimint_core::envs::is_env_var_set_opt(FM_PKARR_RELAYS_ENABLE_ENV).unwrap_or(true);
67
68    if !dht_enabled && !relays_enabled {
69        info!(
70            target: LOG_NET_API,
71            pkarr_id = %keypair.to_z32(),
72            "Pkarr publishing disabled (both DHT and relays disabled)"
73        );
74        return Ok(());
75    }
76
77    let mut builder = pkarr::Client::builder();
78    if !dht_enabled {
79        builder.no_dht();
80    }
81    if !relays_enabled {
82        builder.no_relays();
83    }
84    let client = builder.build()?;
85
86    let db = db.clone();
87    let our_peer_id = cfg.local.identity;
88    let consensus_cfg = cfg.consensus.clone();
89
90    info!(
91        target: LOG_NET_API,
92        pkarr_id = %keypair.to_z32(),
93        dht_enabled,
94        relays_enabled,
95        "Starting pkarr publish service"
96    );
97
98    tg.spawn_cancellable("pkarr-publish", async move {
99        sleep(Duration::from_secs(INITIAL_DELAY_SECS)).await;
100
101        loop {
102            let api_urls = super::announcement::get_api_urls(&db, &consensus_cfg).await;
103            let our_url = api_urls.get(&our_peer_id);
104
105            let success = if let Some(url) = our_url {
106                publish_api_url(&client, &keypair, &url.to_string()).await
107            } else {
108                debug!(
109                    target: LOG_NET_API,
110                    "No API URL found for our peer, skipping pkarr publish"
111                );
112                false
113            };
114
115            let delay = if success {
116                Duration::from_secs(PUBLISH_INTERVAL_SECS)
117            } else {
118                Duration::from_secs(FAILURE_RETRY_SECS)
119            };
120
121            sleep(delay).await;
122        }
123    });
124
125    Ok(())
126}
127
128async fn publish_api_url(client: &pkarr::Client, keypair: &pkarr::Keypair, url: &str) -> bool {
129    let signed_packet = match build_signed_packet(keypair, url) {
130        Ok(packet) => packet,
131        Err(e) => {
132            warn!(
133                target: LOG_NET_API,
134                err = %e.fmt_compact(),
135                "Failed to build pkarr signed packet"
136            );
137            return false;
138        }
139    };
140
141    match client.publish(&signed_packet, None).await {
142        Ok(()) => {
143            info!(
144                target: LOG_NET_API,
145                url,
146                pkarr_id = %keypair.to_z32(),
147                "Published API URL to pkarr"
148            );
149            true
150        }
151        Err(e) => {
152            debug!(
153                target: LOG_NET_API,
154                err = %e.fmt_compact(),
155                "Failed to publish to pkarr, will retry"
156            );
157            false
158        }
159    }
160}
161
162fn build_signed_packet(
163    keypair: &pkarr::Keypair,
164    url: &str,
165) -> Result<SignedPacket, pkarr::errors::SignedPacketBuildError> {
166    SignedPacket::builder()
167        .txt(
168            pkarr::dns::Name::new_unchecked("fedimint_api"),
169            url.try_into().expect("API URL should be valid TXT data"),
170            TXT_RECORD_TTL,
171        )
172        .sign(keypair)
173}