Skip to content

Implement Distinct RNG Sequences #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

sangbida
Copy link

@sangbida sangbida commented Apr 29, 2025

Overview

This PR introduces a deterministic yet unique random number generation system for the Lightning Network simulation. Each NetworkGraphView now maintains a base seed, while individual nodes receive their own RNG instances derived from this base seed and salted with their public keys. This ensures consistent but varied behavior across the network while preserving reproducibility.

Changes

  • Added seed field to MutRng struct
  • Implemented salted method that creates node-specific RNGs by combining the original seed with the node's public key
  • Updated random_activity_nodes to use salted RNGs for each node

@sangbida sangbida changed the title Add support for salted rng for each node Implement Distinct RNG Sequences Apr 29, 2025
@sangbida sangbida force-pushed the sangbida/distinct-rng branch from 61929d6 to cfd394a Compare April 29, 2025 23:07
pub fn salted(&self, pubkey: &PublicKey) -> Self {
// Get the pubkey bytes
let pubkey_bytes = pubkey.serialize();

Copy link
Author

Choose a reason for hiding this comment

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

Would appreciate thoughts on this approach to salting

Copy link
Contributor

Choose a reason for hiding this comment

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

I asked the ai gods and they suggested just using a straight-up hash:

let mut hasher = DefaultHasher::new();
source.pubkey.hash(&mut hasher);
let salt = hasher.finish();

Since we only care about determinism here (not about salt that provides good entropy), I don't think it matters much which approach we use provided they yield distinct values per pubkey.

Copy link
Author

@sangbida sangbida May 2, 2025

Choose a reason for hiding this comment

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

Ah okay, we don't care to salt the base seed, right? As in, it's okay for the rng to be something like: Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(salt)))) or do we want to do something like base_seed ^ salt

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that we do want to include the base seed here just so that there's an option for folks to run with different, fixed seeds? If we just use pubkey, we'll only ever get one set of payments per pubkey, vs if we include the base seed we can change it (while keeping it deterministic)

Copy link
Contributor

@carlaKC carlaKC left a comment

Choose a reason for hiding this comment

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

Great start on this!

struct MutRng(MutRngType);
struct MutRng {
rng: MutRngType,
seed: Option<u64>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Storing this so that we can split up rng = new() and rng.salted isn't very appealing to me - if we only need a field to instantiate the object, maybe we're instantiating it wrong.

I think it would be okay to have:

  • new: as is, used to create MutRng
  • get_salted_seed(u64, pubkey) => u64: a helper to generate the seed

The caller is responsible for setting up their own saltyseed and then calling new, but I think that's fine

Comment on lines 516 to 517
// Get the pubkey bytes
let pubkey_bytes = pubkey.serialize();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: unnecessary comment (the code is so simple it's self-documenting IMO)

pub fn salted(&self, pubkey: &PublicKey) -> Self {
// Get the pubkey bytes
let pubkey_bytes = pubkey.serialize();

Copy link
Contributor

Choose a reason for hiding this comment

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

I asked the ai gods and they suggested just using a straight-up hash:

let mut hasher = DefaultHasher::new();
source.pubkey.hash(&mut hasher);
let salt = hasher.finish();

Since we only care about determinism here (not about salt that provides good entropy), I don't think it matters much which approach we use provided they yield distinct values per pubkey.

let mut rng1 = salted_rng_1.rng.lock().unwrap();
let mut rng2 = salted_rng_2.rng.lock().unwrap();

for _ in 0..10 {
Copy link
Contributor

Choose a reason for hiding this comment

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

nice!

fn create_salted_mut_rng() {
let base_rng = MutRng::new(Some(42));

let pk1 = PublicKey::from_slice(&[
Copy link
Contributor

Choose a reason for hiding this comment

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

While it seems a bit deranged to use random values in a test about determinism, I think we can use get_random_keypair here to get different pubkeys on each run.

It should never fail for valid pubkeys, so we're basically fuzzing this incredibly slowly.

Copy link
Author

Choose a reason for hiding this comment

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

Ah thanks for this! I was looking for a function like this! I was going to use this but didn't want to unecessarily add to Cargo.toml.

Comment on lines 974 to 975
// Create a salted RNG for this node based on its pubkey
let salted_rng = self.cfg.seeded_rng.salted(&node_info.pubkey);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, we already have our RNG in SimulationConfig which is why we need salted, got it!

Let's do a refactor commit before this one that provides the seed in SimulationConfig and then we can set up our own RNGs however we like internally. The caller already just passes in the seed and lets us handle it so I think it's fine to change.

Copy link
Author

Choose a reason for hiding this comment

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

Just to clarify we want to remove seeded_rng from the SimulationCfg and replace it with the seed, then generate the rng inside random_acitivity_nodes?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that's what I was thinking - that way we've got the seed available to create our salted RNGs and don't need to store it in the RNG itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants