Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4adfd50
chore(release): v0.208.0 — temp-dir leak lane (#397, #370)
MichaelTaylor3d Sep 1, 2026
0958d5e
fix(test): own dig-wallet and profile-sync scratch dirs with tempfile…
MichaelTaylor3d Sep 1, 2026
a3f09df
fix(test): own peer-pool and server harness scratch trees with tempfi…
MichaelTaylor3d Sep 1, 2026
49624d9
fix(test): own control, state, pairing and census scratch dirs with t…
MichaelTaylor3d Sep 1, 2026
9b99114
fix(test): own updater, beacon, drift-guard and logging scratch trees
MichaelTaylor3d Sep 1, 2026
ebe66a7
test(profile-sync): assert the scratch tree is removed on drop and on…
MichaelTaylor3d Sep 1, 2026
3f1778f
fix(test): thread the pool scratch guard through fresh_pool_handle
MichaelTaylor3d Sep 1, 2026
1712bae
fix(test): finish the spend-audit e2e and warm-locator scratch guards
MichaelTaylor3d Sep 1, 2026
9766c13
fix(test): thread the mirror-expiry scratch guard and drop the now-de…
MichaelTaylor3d Sep 1, 2026
9bc07f7
chore(release): 0.212.0
MichaelTaylor3d Sep 1, 2026
bc9b391
fix(test): finish the control, census, pairing and spends-cli scratch…
MichaelTaylor3d Sep 1, 2026
cd73468
fix(test): pass scratch paths to set_var rather than the guard itself
MichaelTaylor3d Sep 1, 2026
6b8efc9
fix(test): use the pool scratch guard's path in the gossip config
MichaelTaylor3d Sep 1, 2026
0a609c2
fix(test): use the reannounce scratch guard's path in the gossip config
MichaelTaylor3d Sep 1, 2026
dad1120
chore(test): drop the uniqueness counters tempfile's suffix replaced
MichaelTaylor3d Sep 1, 2026
f0224b7
fix(test): return the spends-cli scratch guard with the log it points…
MichaelTaylor3d Sep 1, 2026
8bc8db4
chore: merge origin/main; bump 0.216.0
MichaelTaylor3d Sep 1, 2026
4c5450c
fix(test): own the spend-audit and warm-locator scratch trees with te…
MichaelTaylor3d Sep 1, 2026
ede3479
test(profile-sync): record the separate mutation that proves the unwi…
MichaelTaylor3d Sep 1, 2026
2225313
style: rustfmt the profile-sync scratch-guard test additions
MichaelTaylor3d Sep 1, 2026
0388e9d
style: rustfmt this lane's test edits; drop a stray scratch file from…
MichaelTaylor3d Sep 1, 2026
a8449ab
fix(test): use the control scratch guard's path; clear the pairing ne…
MichaelTaylor3d Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.213.0"
version = "0.216.0"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
41 changes: 21 additions & 20 deletions crates/dig-node-core/src/capsule_warm_locator_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
//! [`crate::download::NodeContent::warm_provider_locator`], which is the handle production uses.

use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use async_trait::async_trait;
Expand All @@ -37,17 +36,13 @@ fn hex32(bytes: [u8; 32]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}

fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"dig-node-warmloc-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos()
));
std::fs::create_dir_all(&dir).expect("tempdir");
dir
/// The directory is OWNED by the returned guard: `TempDir`'s `Drop` removes the tree,
/// including on an unwind, so a failing assertion cannot leak it (dig-node#370).
fn temp_dir(tag: &str) -> tempfile::TempDir {
tempfile::Builder::new()
.prefix(&format!("dig-node-warmloc-{tag}-"))
.tempdir()
.expect("tempdir")
}

/// Confirms the fixture generation, so the warm passes its chain gate and reaches the locate step
Expand Down Expand Up @@ -125,7 +120,13 @@ impl AnnounceHolder for SilentAnnounce {
/// The locator is built through [`NodeContent::provider_locator_chain`] — the union, the
/// self-exclusion and the capsule fallback `for_dht` installs — so these fixtures drive the layers
/// production drives.
fn node_with_pool_peer(pool_peer: &str, self_peer_id: Option<String>) -> Arc<NodeContent> {
///
/// The scratch guard is returned with the node rather than bound here: dropping it at the end of
/// THIS function would delete the directory the returned `NodeContent` is rooted at.
fn node_with_pool_peer(
pool_peer: &str,
self_peer_id: Option<String>,
) -> (Arc<NodeContent>, tempfile::TempDir) {
let dir = temp_dir("engine");
let content = NodeContent::new(
NodeContent::provider_locator_chain(
Expand All @@ -135,13 +136,13 @@ fn node_with_pool_peer(pool_peer: &str, self_peer_id: Option<String>) -> Arc<Nod
Arc::new(MockRangeTransport::new(MockContent::even(4, 1))),
MissMode::Redirect,
self_peer_id,
&dir,
dir.path(),
);
content.connected_pool().lock().expect("pool lock").insert(
pool_peer.to_string(),
vec!["10.0.0.9:9444".parse::<SocketAddr>().expect("test address")],
);
content
(content, dir)
}

/// A warmer built over `content`'s PRODUCTION warm locator and a recording transport.
Expand Down Expand Up @@ -182,9 +183,9 @@ fn warmer_over(
async fn a_warm_reaches_a_holder_that_only_the_connected_pool_can_name() {
let dir = temp_dir("reachability");
let holder = mock_peer_hex(9);
let content = node_with_pool_peer(&holder, Some(mock_peer_hex(1)));
let (content, _scratch) = node_with_pool_peer(&holder, Some(mock_peer_hex(1)));
let transport = Arc::new(RecordingModuleTransport::default());
let warmer = warmer_over(&content, Arc::clone(&transport), &dir);
let warmer = warmer_over(&content, Arc::clone(&transport), dir.path());

warmer.warm(&hex32(STORE), &hex32(ROOT)).await;

Expand Down Expand Up @@ -223,9 +224,9 @@ async fn a_warm_reaches_a_holder_that_only_the_connected_pool_can_name() {
async fn a_pool_entry_naming_this_node_is_never_a_dial_candidate() {
let dir = temp_dir("self-exclusion");
let me = mock_peer_hex(9);
let content = node_with_pool_peer(&me, Some(me.clone()));
let (content, _scratch) = node_with_pool_peer(&me, Some(me.clone()));
let transport = Arc::new(RecordingModuleTransport::default());
let warmer = warmer_over(&content, Arc::clone(&transport), &dir);
let warmer = warmer_over(&content, Arc::clone(&transport), dir.path());

warmer.warm(&hex32(STORE), &hex32(ROOT)).await;

Expand All @@ -252,7 +253,7 @@ async fn a_pool_entry_naming_this_node_is_never_a_dial_candidate() {
#[tokio::test]
async fn discovery_still_excludes_the_pool_that_the_warm_locator_includes() {
let peer = mock_peer_hex(9);
let content = node_with_pool_peer(&peer, Some(mock_peer_hex(1)));
let (content, _scratch) = node_with_pool_peer(&peer, Some(mock_peer_hex(1)));
let capsule = dig_dht::ContentId::capsule(STORE, ROOT);

let discovered = content
Expand Down
84 changes: 52 additions & 32 deletions crates/dig-node-core/src/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3618,13 +3618,16 @@ pub(crate) mod tests {
let peer_port = peer_rpc.local_addr().unwrap().port();

// The gossip pool on its OWN OS-assigned ephemeral port (a different socket).
let dir = std::env::temp_dir().join(format!("dig-node-wuc-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
// Owned by the guard: the tree goes away on drop and on an unwind (dig-node#370).
let dir = tempfile::Builder::new()
.prefix("dig-node-wuc-")
.tempdir()
.expect("a pool cert dir");
let cfg = dig_gossip::GossipConfig {
network_id: chia_protocol::Bytes32::new([1u8; 32]),
cert_path: dir.join("node.cert").display().to_string(),
key_path: dir.join("node.key").display().to_string(),
peers_file_path: dir.join("peers.json"),
cert_path: dir.path().join("node.cert").display().to_string(),
key_path: dir.path().join("node.key").display().to_string(),
peers_file_path: dir.path().join("peers.json"),
peer_pool: Some(dig_gossip::PeerPoolConfig::default()),
listen_addr: fresh_pool_listen_addr().await,
..Default::default()
Expand Down Expand Up @@ -3660,13 +3663,16 @@ pub(crate) mod tests {
/// authority parses; the ONLY thing wrong with this anchor is that nothing answers there.
#[tokio::test]
async fn a_node_survives_every_bootstrap_anchor_being_unreachable() {
let dir = std::env::temp_dir().join(format!("dig-node-bootstrap-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
// Owned by the guard: the tree goes away on drop and on an unwind (dig-node#370).
let dir = tempfile::Builder::new()
.prefix("dig-node-bootstrap-")
.tempdir()
.expect("a pool cert dir");
let cfg = dig_gossip::GossipConfig {
network_id: chia_protocol::Bytes32::new([3u8; 32]),
cert_path: dir.join("node.cert").display().to_string(),
key_path: dir.join("node.key").display().to_string(),
peers_file_path: dir.join("peers.json"),
cert_path: dir.path().join("node.cert").display().to_string(),
key_path: dir.path().join("node.key").display().to_string(),
peers_file_path: dir.path().join("peers.json"),
peer_pool: Some(dig_gossip::PeerPoolConfig::default()),
listen_addr: fresh_pool_listen_addr().await,
..Default::default()
Expand Down Expand Up @@ -3716,13 +3722,16 @@ pub(crate) mod tests {
// same reservation the node drives.
#[tokio::test]
async fn wire_relay_reservation_shares_one_status_with_the_pool() {
let dir = std::env::temp_dir().join(format!("dig-node-wuc-share-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
// Owned by the guard: the tree goes away on drop and on an unwind (dig-node#370).
let dir = tempfile::Builder::new()
.prefix("dig-node-wuc-share-")
.tempdir()
.expect("a pool cert dir");
let cfg = dig_gossip::GossipConfig {
network_id: chia_protocol::Bytes32::new([2u8; 32]),
cert_path: dir.join("node.cert").display().to_string(),
key_path: dir.join("node.key").display().to_string(),
peers_file_path: dir.join("peers.json"),
cert_path: dir.path().join("node.cert").display().to_string(),
key_path: dir.path().join("node.key").display().to_string(),
peers_file_path: dir.path().join("peers.json"),
peer_pool: Some(dig_gossip::PeerPoolConfig::default()),
listen_addr: fresh_pool_listen_addr().await,
..Default::default()
Expand Down Expand Up @@ -3801,7 +3810,7 @@ pub(crate) mod tests {
/// than a log line, which prints on the broken path too.
#[tokio::test]
async fn a_stale_relayed_slot_does_not_refuse_the_direct_adoption() {
let handle = fresh_pool_handle("readopt-supersede", [11u8; 32]).await;
let (handle, _handle_dir) = fresh_pool_handle("readopt-supersede", [11u8; 32]).await;
let peer = [0xAB; 32];

// A relayed adoption lands first, then its session DIES (the server half is dropped) — the
Expand Down Expand Up @@ -3924,7 +3933,7 @@ pub(crate) mod tests {
/// the CLI renderer, or a helper written and never called, leaves this red.
#[tokio::test]
async fn a_relay_peer_with_a_wildcard_pool_address_reports_no_address() {
let handle = fresh_pool_handle("wildcard-address", [23u8; 32]).await;
let (handle, _handle_dir) = fresh_pool_handle("wildcard-address", [23u8; 32]).await;

// The peer seen in the wild: relayed, with dig-nat's unspecified remote.
let (wildcard, _wildcard_server) = loopback_nat_conn(
Expand Down Expand Up @@ -4014,7 +4023,7 @@ pub(crate) mod tests {
/// state before any peer connects). Uses a real `GossipHandle` — the same type the node retains.
#[tokio::test]
async fn connected_peers_json_is_empty_for_a_fresh_pool() {
let handle = fresh_pool_handle("cpjson-empty", [3u8; 32]).await;
let (handle, _handle_dir) = fresh_pool_handle("cpjson-empty", [3u8; 32]).await;
assert!(connected_peers_json(&handle).is_empty());
}

Expand All @@ -4023,7 +4032,7 @@ pub(crate) mod tests {
/// path the RPC arm returns as a control error.
#[tokio::test]
async fn connect_peer_rejects_a_non_address_non_peer_id_argument() {
let handle = fresh_pool_handle("connect-bad-arg", [4u8; 32]).await;
let (handle, _handle_dir) = fresh_pool_handle("connect-bad-arg", [4u8; 32]).await;
let err = connect_peer(&handle, "not-an-address").await.unwrap_err();
assert!(err.contains("dialable address"), "got: {err}");
}
Expand Down Expand Up @@ -4108,8 +4117,9 @@ pub(crate) mod tests {
// Same network_id on both — a mismatch would be rejected at handshake. B binds a concrete
// IPv6 loopback (§5.2 IPv6-first) so the inbound accept registers on every platform.
let loopback_v6 = "[::1]:0".parse().expect("parse [::1]:0");
let node_a = fresh_pool_handle("loopback-a", [0x5au8; 32]).await;
let node_b = fresh_pool_handle_on("loopback-b", [0x5au8; 32], loopback_v6).await;
let (node_a, _node_a_dir) = fresh_pool_handle("loopback-a", [0x5au8; 32]).await;
let (node_b, _node_b_dir) =
fresh_pool_handle_on("loopback-b", [0x5au8; 32], loopback_v6).await;

let a_peer_id = hex::encode(node_a.local_peer_id().expect("node A local_peer_id"));
let b_port = node_b
Expand Down Expand Up @@ -4172,7 +4182,7 @@ pub(crate) mod tests {
/// mirroring the connect arg-validation path — no network touch, no hang.
#[tokio::test]
async fn disconnect_peer_rejects_a_malformed_peer_id() {
let handle = fresh_pool_handle("disconnect-bad-arg", [7u8; 32]).await;
let (handle, _handle_dir) = fresh_pool_handle("disconnect-bad-arg", [7u8; 32]).await;
let err = disconnect_peer(&handle, "not-hex").await.unwrap_err();
assert!(err.contains("64-hex peer_id"), "got: {err}");
}
Expand All @@ -4185,7 +4195,7 @@ pub(crate) mod tests {
/// variant, a cross-family contract release, so this PR extends the existing peerStatus surface).
#[tokio::test]
async fn pool_stats_json_reports_the_pool_posture() {
let handle = fresh_pool_handle("pool-stats", [9u8; 32]).await;
let (handle, _handle_dir) = fresh_pool_handle("pool-stats", [9u8; 32]).await;
let stats = pool_stats_json(&handle);
assert_eq!(stats["connected"], 0, "a fresh pool has no connected peers");
assert_eq!(stats["in_flight"], 0, "no dials are in flight yet");
Expand All @@ -4207,10 +4217,13 @@ pub(crate) mod tests {

/// Build a real, freshly-started `GossipHandle` on the production-shaped dual-stack unspecified
/// bind (`[::]:0`, §5.2) for the pool-handle tests.
/// The scratch guard rides along with the handle, for the reason
/// [`fresh_pool_handle_on`] documents: the started pool reads its cert files for its
/// whole lifetime, so the caller holds both.
pub(crate) async fn fresh_pool_handle(
tag: &str,
network: [u8; 32],
) -> dig_gossip::GossipHandle {
) -> (dig_gossip::GossipHandle, tempfile::TempDir) {
fresh_pool_handle_on(tag, network, fresh_pool_listen_addr().await).await
}

Expand All @@ -4221,27 +4234,34 @@ pub(crate) mod tests {
/// inbound loopback connections into the pool (the native-tls dual-stack accept quirk — the same
/// family of `[::]`-v6only issue tracked for the extension-offline path), whereas a concrete
/// loopback bind does, on every platform. Production still binds dual-stack `[::]` (`run_peer_network`).
/// Returns the guard alongside the handle: the started pool reads its cert, key and
/// peers files for its whole lifetime, so the directory must outlive the handle rather
/// than this function. The caller holds both, and `TempDir`'s `Drop` removes the tree
/// when the test ends — including on an unwind (dig-node#370).
pub(crate) async fn fresh_pool_handle_on(
tag: &str,
network: [u8; 32],
listen_addr: std::net::SocketAddr,
) -> dig_gossip::GossipHandle {
let dir = std::env::temp_dir().join(format!("dig-node-{tag}-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
) -> (dig_gossip::GossipHandle, tempfile::TempDir) {
let dir = tempfile::Builder::new()
.prefix(&format!("dig-node-{tag}-"))
.tempdir()
.expect("a pool cert dir");
let cfg = dig_gossip::GossipConfig {
network_id: chia_protocol::Bytes32::new(network),
cert_path: dir.join("node.cert").display().to_string(),
key_path: dir.join("node.key").display().to_string(),
peers_file_path: dir.join("peers.json"),
cert_path: dir.path().join("node.cert").display().to_string(),
key_path: dir.path().join("node.key").display().to_string(),
peers_file_path: dir.path().join("peers.json"),
peer_pool: Some(dig_gossip::PeerPoolConfig::default()),
listen_addr,
..Default::default()
};
dig_gossip::GossipService::new(cfg)
let handle = dig_gossip::GossipService::new(cfg)
.expect("gossip config")
.start()
.await
.expect("gossip start")
.expect("gossip start");
(handle, dir)
}

#[test]
Expand Down
Loading
Loading