fedimint_server/net/api/
pkarr_publish.rs1use 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
17const 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
25pub 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
37pub fn pkarr_id_z32(broadcast_sk: &SecretKey) -> String {
40 derive_pkarr_keypair(broadcast_sk).to_z32()
41}
42
43pub 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}