From 5b211158f7f2d4e17e79271b2ba2b4cfb856ecd7 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 5 Jan 2026 11:31:23 +0100 Subject: [PATCH 1/2] Make test block connect style configurable for deterministic runs Tests previously selected a random block connect style to improve coverage. However, this randomness made test output non-deterministic and significantly cluttered diffs when comparing logs before and after changes. To address this, an LDK_TEST_CONNECT_STYLE environment variable is added to override the random selection and enable deterministic test runs. Note that broader coverage may be better achieved via targeted tests per connect style or a test matrix cycling through all styles, but this change focuses on improving reproducibility and debuggability. --- CONTRIBUTING.md | 14 +++++++++++++ lightning/src/ln/functional_test_utils.rs | 24 ++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1bc431a3110..e7825ac3f17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -176,6 +176,20 @@ Fuzzing is heavily encouraged: you will find all related material under `fuzz/` Mutation testing is work-in-progress; any contribution there would be warmly welcomed. +### Environment Variables + +* `LDK_TEST_CONNECT_STYLE` - Override the random block connect style used in tests for deterministic runs. Valid values: + * `BEST_BLOCK_FIRST` + * `BEST_BLOCK_FIRST_SKIPPING_BLOCKS` + * `BEST_BLOCK_FIRST_REORGS_ONLY_TIP` + * `TRANSACTIONS_FIRST` + * `TRANSACTIONS_FIRST_SKIPPING_BLOCKS` + * `TRANSACTIONS_DUPLICATIVELY_FIRST_SKIPPING_BLOCKS` + * `HIGHLY_REDUNDANT_TRANSACTIONS_FIRST_SKIPPING_BLOCKS` + * `TRANSACTIONS_FIRST_REORGS_ONLY_TIP` + * `FULL_BLOCK_VIA_LISTEN` + * `FULL_BLOCK_DISCONNECTIONS_SKIPPING_VIA_LISTEN` + C/C++ Bindings -------------- diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index e072deb6a97..e9cb13dbd2a 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -4563,7 +4563,29 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>( let mut nodes = Vec::new(); let chan_count = Rc::new(RefCell::new(0)); let payment_count = Rc::new(RefCell::new(0)); - let connect_style = Rc::new(RefCell::new(ConnectStyle::random_style())); + + let connect_style = Rc::new(RefCell::new(match std::env::var("LDK_TEST_CONNECT_STYLE") { + Ok(val) => match val.as_str() { + "BEST_BLOCK_FIRST" => ConnectStyle::BestBlockFirst, + "BEST_BLOCK_FIRST_SKIPPING_BLOCKS" => ConnectStyle::BestBlockFirstSkippingBlocks, + "BEST_BLOCK_FIRST_REORGS_ONLY_TIP" => ConnectStyle::BestBlockFirstReorgsOnlyTip, + "TRANSACTIONS_FIRST" => ConnectStyle::TransactionsFirst, + "TRANSACTIONS_FIRST_SKIPPING_BLOCKS" => ConnectStyle::TransactionsFirstSkippingBlocks, + "TRANSACTIONS_DUPLICATIVELY_FIRST_SKIPPING_BLOCKS" => { + ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks + }, + "HIGHLY_REDUNDANT_TRANSACTIONS_FIRST_SKIPPING_BLOCKS" => { + ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks + }, + "TRANSACTIONS_FIRST_REORGS_ONLY_TIP" => ConnectStyle::TransactionsFirstReorgsOnlyTip, + "FULL_BLOCK_VIA_LISTEN" => ConnectStyle::FullBlockViaListen, + "FULL_BLOCK_DISCONNECTIONS_SKIPPING_VIA_LISTEN" => { + ConnectStyle::FullBlockDisconnectionsSkippingViaListen + }, + _ => panic!("Unknown ConnectStyle '{}'", val), + }, + Err(_) => ConnectStyle::random_style(), + })); for i in 0..node_count { let dedicated_entropy = DedicatedEntropy(RandomBytes::new([i as u8; 32])); From 075bc349f3f0684c5ea0424c7030dcc949d3c698 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 7 Jan 2026 13:22:33 +0100 Subject: [PATCH 2/2] Make test hash map iteration order configurable for deterministic runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `RandomState` hasher implementation for tests that supports deterministic behavior via the `LDK_TEST_DETERMINISTIC_HASHES=1` environment variable. When set, hash maps use fixed keys ensuring consistent iteration order across test runs. By default, tests continue to use std's RandomState for random hashing, keeping test behavior close to production. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CONTRIBUTING.md | 2 + lightning/src/util/hash_tables.rs | 67 ++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7825ac3f17..d837c873efa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -190,6 +190,8 @@ welcomed. * `FULL_BLOCK_VIA_LISTEN` * `FULL_BLOCK_DISCONNECTIONS_SKIPPING_VIA_LISTEN` +* `LDK_TEST_DETERMINISTIC_HASHES` - When set to `1`, uses deterministic hash map iteration order in tests. This ensures consistent test output across runs, useful for comparing logs before and after changes. + C/C++ Bindings -------------- diff --git a/lightning/src/util/hash_tables.rs b/lightning/src/util/hash_tables.rs index 00341d57b45..b6555975191 100644 --- a/lightning/src/util/hash_tables.rs +++ b/lightning/src/util/hash_tables.rs @@ -6,10 +6,75 @@ pub use hashbrown::hash_map; mod hashbrown_tables { - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(test)))] mod hasher { pub use std::collections::hash_map::RandomState; } + #[cfg(all(feature = "std", test))] + mod hasher { + #![allow(deprecated)] // hash::SipHasher was deprecated in favor of something only in std. + use core::hash::{BuildHasher, Hasher}; + + /// A [`BuildHasher`] for tests that supports deterministic behavior via environment variable. + /// + /// When `LDK_TEST_DETERMINISTIC_HASHES` is set, uses fixed keys for deterministic iteration. + /// Otherwise, delegates to std's RandomState for random hashing. + #[derive(Clone)] + pub enum RandomState { + Std(std::collections::hash_map::RandomState), + Deterministic, + } + + impl RandomState { + pub fn new() -> RandomState { + if std::env::var("LDK_TEST_DETERMINISTIC_HASHES").map(|v| v == "1").unwrap_or(false) + { + RandomState::Deterministic + } else { + RandomState::Std(std::collections::hash_map::RandomState::new()) + } + } + } + + impl Default for RandomState { + fn default() -> RandomState { + RandomState::new() + } + } + + /// A hasher wrapper that delegates to either std's DefaultHasher or a deterministic SipHasher. + pub enum RandomStateHasher { + Std(std::collections::hash_map::DefaultHasher), + Deterministic(core::hash::SipHasher), + } + + impl Hasher for RandomStateHasher { + fn finish(&self) -> u64 { + match self { + RandomStateHasher::Std(h) => h.finish(), + RandomStateHasher::Deterministic(h) => h.finish(), + } + } + fn write(&mut self, bytes: &[u8]) { + match self { + RandomStateHasher::Std(h) => h.write(bytes), + RandomStateHasher::Deterministic(h) => h.write(bytes), + } + } + } + + impl BuildHasher for RandomState { + type Hasher = RandomStateHasher; + fn build_hasher(&self) -> RandomStateHasher { + match self { + RandomState::Std(s) => RandomStateHasher::Std(s.build_hasher()), + RandomState::Deterministic => { + RandomStateHasher::Deterministic(core::hash::SipHasher::new_with_keys(0, 0)) + }, + } + } + } + } #[cfg(not(feature = "std"))] mod hasher { #![allow(deprecated)] // hash::SipHasher was deprecated in favor of something only in std.