Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@ 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`

* `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
--------------

Expand Down
24 changes: 23 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused how this works in our no-std tests, like how CI is passing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-std tests are actually (secretly) built with std enabled. Its useful lol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. What was the original reason for that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC cargo actually forces it on us, but we take advantage of it. Its possible we screwed up our feature flags at some point and accidentally did it, but either way its useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some way to make this more discoverable? Maybe CONTRIBUTING.md? I think a comment would also be useful to describe why a dev would want to set this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point to add more docs. It is a bit confronting that we add and document an env var that underlines that there are non-deterministic unit tests, instead of making them deterministic.

We could also think about alternatives such as a 'deterministic_test' cfg flag, which we can then also use to add for example sorting when we iterate over hashmap/set keys?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added more docs and also added a commit for deterministic hash table iteration.

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]));
Expand Down
67 changes: 66 additions & 1 deletion lightning/src/util/hash_tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, test determinism is great, but test coverage is better. I'm a bit skeptical that the solution to "sometimes my tests are flaky due to hashmap iteration order" is to fix the hashmap iteration order, rather than fixing the tests. Having an option in the env like you do above to fix hashmap iteration order to make debugging easier seems reasonable, however.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. It's not about flakey tests btw, but about making test output comparable across runs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I've def had it make tests flaky before :)

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.
Expand Down
Loading