Skip to content

Commit f6174e0

Browse files
author
techartdev
committed
release: v0.2.0 manifest DHT reannouncement, immediate post-publish replication, 5s connect timeouts, dual-transport bootstrap, comprehensive tracing
1 parent af9dddc commit f6174e0

12 files changed

Lines changed: 211 additions & 15 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ resolver = "2"
1111
[workspace.package]
1212
edition = "2024"
1313
license = "MPL-2.0"
14-
version = "0.1.0"
14+
version = "0.2.0"
1515
repository = "https://github.com/techartdev/scp2p"
1616

1717
[workspace.dependencies]

app/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "scp2p-app",
33
"private": true,
4-
"version": "0.1.0",
4+
"version": "0.2.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
@@ -29,4 +29,4 @@
2929
"typescript": "^5.7.0",
3030
"vite": "^6.0.0"
3131
}
32-
}
32+
}

app/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri/schemas/tauri-conf-v2.json",
33
"productName": "SCP2P",
4-
"version": "0.1.0",
4+
"version": "0.2.0",
55
"identifier": "com.scp2p.desktop",
66
"build": {
77
"frontendDist": "../dist",

crates/scp2p-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ clap = { workspace = true, features = ["env"] }
1515
ed25519-dalek.workspace = true
1616
hex.workspace = true
1717
rand.workspace = true
18-
scp2p-core = { path = "../scp2p-core", version = "0.1.0" }
18+
scp2p-core = { path = "../scp2p-core", version = "0.2.0" }
1919
tokio.workspace = true
2020
inquire = "0.7"
2121
indicatif = "0.17"

crates/scp2p-core/src/api/node_dht.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ impl NodeHandle {
379379
transport: &T,
380380
seed_peers: &[PeerAddr],
381381
) -> anyhow::Result<usize> {
382+
// Re-populate ephemeral DHT with share heads + manifests for
383+
// shares we have *published*, so they survive app restarts.
384+
let _ = self.reannounce_published_share_data().await;
385+
382386
// Refresh share heads for public subscriptions so they survive
383387
// after the original publisher goes offline.
384388
let _ = self.reannounce_subscribed_share_heads().await;

crates/scp2p-core/src/api/node_net.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,4 +1462,58 @@ impl NodeHandle {
14621462
}
14631463
Ok(refreshed)
14641464
}
1465+
1466+
/// Re-populate the in-memory DHT with share heads **and** manifests for
1467+
/// shares we have *published*. `reannounce_subscribed_share_heads` only
1468+
/// covers the subscriber side (iterates `subscriptions`); this covers the
1469+
/// publisher side so that data survives an app restart where the ephemeral
1470+
/// DHT is empty.
1471+
pub async fn reannounce_published_share_data(&self) -> anyhow::Result<usize> {
1472+
let now = super::helpers::now_unix_secs()?;
1473+
let mut state = self.state.write().await;
1474+
let mut refreshed = 0usize;
1475+
1476+
// Collect keys first to avoid borrow issues.
1477+
let heads: Vec<([u8; 32], crate::manifest::ShareHead)> = state
1478+
.published_share_heads
1479+
.iter()
1480+
.map(|(k, v)| (*k, v.clone()))
1481+
.collect();
1482+
1483+
for (share_id, head) in heads {
1484+
// 1. Re-announce the share head itself.
1485+
let head_key = share_head_key(&ShareId(share_id));
1486+
let head_bytes = match crate::cbor::to_vec(&head) {
1487+
Ok(b) => b,
1488+
Err(_) => continue,
1489+
};
1490+
state
1491+
.dht
1492+
.store(head_key, head_bytes, crate::dht::DEFAULT_TTL_SECS, now)?;
1493+
refreshed += 1;
1494+
1495+
// 2. Re-announce the manifest so subscribers behind NAT can
1496+
// fetch it from the relay / DHT.
1497+
let manifest_id = head.latest_manifest_id;
1498+
if let Some(manifest) = state.manifest_cache.get(&manifest_id) {
1499+
let manifest_bytes = match crate::cbor::to_vec(manifest) {
1500+
Ok(b) => b,
1501+
Err(_) => continue,
1502+
};
1503+
state.dht.store(
1504+
manifest_loc_key(&ManifestId(manifest_id)),
1505+
manifest_bytes,
1506+
crate::dht::DEFAULT_TTL_SECS,
1507+
now,
1508+
)?;
1509+
refreshed += 1;
1510+
}
1511+
}
1512+
1513+
if refreshed > 0 {
1514+
debug!(refreshed, "reannounce_published_share_data: DHT entries restored");
1515+
}
1516+
1517+
Ok(refreshed)
1518+
}
14651519
}

crates/scp2p-core/src/api/tests.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,17 @@ async fn configured_bootstrap_peers_parse_from_runtime_config() {
147147
.configured_bootstrap_peers()
148148
.await
149149
.expect("configured peers");
150-
assert_eq!(peers.len(), 1);
150+
// Primary TCP peer + QUIC fallback
151+
assert_eq!(peers.len(), 2);
151152
assert_eq!(
152153
peers[0].ip,
153154
"127.0.0.1".parse::<std::net::IpAddr>().expect("ip")
154155
);
155156
assert_eq!(peers[0].port, 7301);
156157
assert_eq!(peers[0].transport, crate::peer::TransportProtocol::Tcp);
158+
// Alternate QUIC variant at port - 1
159+
assert_eq!(peers[1].port, 7300);
160+
assert_eq!(peers[1].transport, crate::peer::TransportProtocol::Quic);
157161
}
158162

159163
#[tokio::test]
@@ -172,7 +176,8 @@ async fn configured_bootstrap_peers_with_transport_prefix() {
172176
.configured_bootstrap_peers()
173177
.await
174178
.expect("configured peers");
175-
assert_eq!(peers.len(), 3);
179+
// 3 explicit + 3 alternate-transport fallbacks
180+
assert_eq!(peers.len(), 6);
176181
assert_eq!(peers[0].transport, crate::peer::TransportProtocol::Quic);
177182
assert_eq!(peers[0].port, 9000);
178183
assert_eq!(peers[1].transport, crate::peer::TransportProtocol::Tcp);
@@ -196,7 +201,8 @@ async fn configured_bootstrap_peers_parse_pubkey_suffix() {
196201
.configured_bootstrap_peers()
197202
.await
198203
.expect("configured peers");
199-
assert_eq!(peers.len(), 2);
204+
// 2 explicit + 2 alternate-transport fallbacks
205+
assert_eq!(peers.len(), 4);
200206
assert_eq!(peers[0].pubkey_hint, Some([0xaa; 32]));
201207
assert_eq!(peers[0].port, 9500);
202208
// Entry without pubkey suffix should have None.

crates/scp2p-desktop/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ hex.workspace = true
1212
rand.workspace = true
1313
serde.workspace = true
1414
ciborium.workspace = true
15-
scp2p-core = { path = "../scp2p-core", version = "0.1.0" }
15+
scp2p-core = { path = "../scp2p-core", version = "0.2.0" }
1616
tokio.workspace = true
1717
tracing.workspace = true

crates/scp2p-desktop/src/app_state.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use tokio::net::UdpSocket;
2222
use tokio::sync::RwLock;
2323
use tokio::task::JoinHandle;
2424
use tokio::time::{self, Duration};
25-
use tracing::info;
25+
use tracing::{info, warn};
2626

2727
use crate::dto::{
2828
CommunityBrowseView, CommunityParticipantView, CommunityView, CreateCommunityResult,
@@ -49,6 +49,10 @@ struct RuntimeState {
4949
dht_republish_task: Option<JoinHandle<()>>,
5050
subscription_sync_task: Option<JoinHandle<()>>,
5151
last_public_shares: Vec<PublicShareView>,
52+
/// Transport used for DHT replication (same instance as background loops).
53+
dht_transport: Option<Arc<dyn scp2p_core::RequestTransport>>,
54+
/// Bootstrap peers used for DHT replication.
55+
dht_bootstrap_peers: Vec<PeerAddr>,
5256
}
5357

5458
const LAN_DISCOVERY_PORT: u16 = 46123;
@@ -188,10 +192,12 @@ impl DesktopAppState {
188192
DHT_LOOP_INTERVAL,
189193
));
190194
state.subscription_sync_task = Some(handle.clone().start_subscription_sync_loop(
191-
transport,
192-
bootstrap,
195+
transport.clone(),
196+
bootstrap.clone(),
193197
DHT_LOOP_INTERVAL,
194198
));
199+
state.dht_transport = Some(transport);
200+
state.dht_bootstrap_peers = bootstrap;
195201
}
196202

197203
state.node = Some(handle);
@@ -711,6 +717,26 @@ impl DesktopAppState {
711717
.context("node is not running")
712718
}
713719

720+
/// Fire-and-forget: run one DHT republish cycle so newly published
721+
/// share heads + manifests reach the relay immediately instead of
722+
/// waiting for the next background interval.
723+
async fn trigger_dht_republish(&self) {
724+
let (node, transport, peers) = {
725+
let state = self.inner.read().await;
726+
match (&state.node, &state.dht_transport) {
727+
(Some(n), Some(t)) => {
728+
(n.clone(), t.clone(), state.dht_bootstrap_peers.clone())
729+
}
730+
_ => return,
731+
}
732+
};
733+
tokio::spawn(async move {
734+
if let Err(e) = node.dht_republish_once(transport.as_ref(), &peers).await {
735+
warn!(error = %e, "immediate DHT republish after publish failed");
736+
}
737+
});
738+
}
739+
714740
async fn sync_peer_targets(&self, node: &NodeHandle) -> anyhow::Result<Vec<PeerAddr>> {
715741
let mut peers = node.configured_bootstrap_peers().await?;
716742
for record in node.peer_records().await {
@@ -802,6 +828,11 @@ impl DesktopAppState {
802828
)
803829
.await?;
804830

831+
// Immediately replicate share head + manifest to DHT peers so
832+
// subscribers can discover the share without waiting for the
833+
// next background republish cycle.
834+
self.trigger_dht_republish().await;
835+
805836
Ok(PublishResultView {
806837
share_id_hex: hex::encode(share.share_id().0),
807838
share_pubkey_hex: hex::encode(share.verifying_key().to_bytes()),
@@ -858,6 +889,9 @@ impl DesktopAppState {
858889
)
859890
.await?;
860891

892+
// Immediately replicate share head + manifest to DHT peers.
893+
self.trigger_dht_republish().await;
894+
861895
Ok(PublishResultView {
862896
share_id_hex: hex::encode(share.share_id().0),
863897
share_pubkey_hex: hex::encode(share.verifying_key().to_bytes()),

0 commit comments

Comments
 (0)