Skip to content

Update jamln to use sim-ln's virtual time for speedup - #133

Merged
carlaKC merged 8 commits into
masterfrom
tokio-des
Jul 9, 2026
Merged

Update jamln to use sim-ln's virtual time for speedup#133
carlaKC merged 8 commits into
masterfrom
tokio-des

Conversation

@carlaKC

@carlaKC carlaKC commented Jun 26, 2026

Copy link
Copy Markdown
Owner

This PR points to a (currently PR'd) version of sim-ln that grants us access to a virtual clock (sped up using a tokio paused runtime).

This updates simulations that take hours to take seconds. I have run a few of our existing attacks on both main and this branch, and the numbers are within 2% of each other. Given that we don't have perfect determinism, this seems reasonable enough to me.

@carlaKC
carlaKC requested a review from elnosh June 26, 2026 19:31

@elnosh elnosh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm having issues running the slow-jam attack - I have been running it for hours and still does not finish.

The behavior of the single-threaded paused tokio runtime is still somewhat obscure to me but I think I'm hitting the issue because of this https://github.com/carlaKC/sim-ln/blob/96c8f6dc3e8bf9d54e60f56f344f1e8e2a11fd00/simln-lib/src/clock.rs#L28-L29 and for slow-jam attack we hold the HTLC for 2 weeks while jamming and then we poll in a loop here

loop {
let jamming_payments_lock = self.jamming_payments.lock().await;
if jamming_payments_lock.is_empty() {
break;
}
self.clock.sleep(Duration::from_secs(60 * 5)).await;
}
Ok(())
until we are done holding the payment. Based on those comments in the sim-ln PR it seems this loop can be problematic.

@carlaKC

carlaKC commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

Based on those comments in the sim-ln PR it seems this loop can be problematic.

Discussed offline: loop creates futures that can be in Pending state, so we're ok here. Also pushed some small (unrelated) fixups to make sure we drop the lock before the sleep.

Speedup we get here is really nice, down from 20 minutes to a few seconds:

time cargo run --release --bin ln-simln-jamming -- \
    --network-dir networks/ln_slow_jam --attack-type slow-jam --attacker-bootstrap 10d \
    --reputation-margin-msat 0 --reputation-margin-expiry-blocks 10 \
    --target-reputation-percent 1 \
    > run.log 2>&1

cargo run --release --bin ln-simln-jamming -- --network-dir  --attack-type     95.54s user 14.62s system 40% cpu 4:33.86 total

One thing that we do lose here (which is a bit of a shame) is that we can't parallelize reading in the large peacetime file. I'm going to work on some changes to how we handle all this unweildy data anyway, so hopefully that's not such an issue long term.

@carlaKC

carlaKC commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

Diff since last review:

carlaKC and others added 3 commits July 8, 2026 10:01
history_from_file split the file into byte-range chunks read on parallel
worker threads. We're about to switch to a single thread runtime, so we
don't get any benefit from this after that. The chunking also had an
off-by-one that silently dropped the final chunk (the whole file when
there was only one chunk), which a plain sequential read can't hit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
If we have rust files called reputation or revenue they'd be ignored
here.
Pull the body of each binary's main() into an async run() that accepts the
clock, leaving main() responsible only for parsing, logging and constructing
the clock. This sets up for handing run() a clock from a virtual-time runtime in
a later commit.

Applies to all three binaries: the attack simulation, the forward builder and
the reputation builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@elnosh elnosh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

cool! simulation ran in ~100s in release mode

Comment thread ln-simln-jamming/src/parsing.rs Outdated
Comment on lines +692 to +698
// Read the whole file sequentially. This previously split the file into byte-range chunks read
// on parallel worker threads, but under the single-threaded virtual-time runtime there is only
// ever one worker, so the chunking never parallelized - it only added complexity, and an
// off-by-one in the chunk loop silently dropped the final chunk (or the entire file when there
// was a single chunk). A plain sequential read cannot have a chunk-boundary bug.
let file = File::open(file_path)?;
let file_size = file.metadata()?.len();
let filter_cutoff = {
if let Some(duration) = filter_duration {
let reader = BufReader::new(file);
let mut csv_reader = csv::Reader::from_reader(reader);
let mut first_record = StringRecord::new();
csv_reader.read_record(&mut first_record)?;
let incoming_add_ts: u64 = first_record[4].parse()?;
Some(incoming_add_ts.add(duration.as_nanos() as u64))
} else {
None
}
};

let mut tasks: Vec<tokio::task::JoinHandle<Result<Vec<BootstrapForward>, BoxError>>> =
Vec::with_capacity(num_chunks);
let mut csv_reader = csv::Reader::from_reader(BufReader::new(file));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yeah I realized after first couple runs that this was slower because it's done single-threaded in the runtime sim-ln creates :(

I don't think there's any reason we can't parallelize the read before actually running the simulation but that will require changes to sim-ln and for how much faster the simulation is already running I'd be fine if left as-is.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I have aspirations to blitz the peacetime_traffic.csv now that we can run sims faster (we can just run peacetime and attack networks in parallel) so shall address in a followup!

Comment on lines +56 to +60
// Run on sim-ln's paused, single-threaded virtual-time runtime so the reputation replay completes instantly.
// Anchor virtual time at the real wall clock because the routing graph rejects channel updates whose timestamps
// are far from real time.
let start_time = SystemTime::now();
block_on_virtual_time(start_time, |clock| run(clock, cli))??;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm wondering why does the reputation-builder need to be ran with the single-threaded runtime from sim-ln? Since we are not running a simulation but rather just replaying events. Maybe to keep the reputation building consistent but there's not much sleep/waits in those events

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fair - probably unnecessary. I'm going to take a look at cleaning up all this setup so will get to this in a follow up!

@carlaKC
carlaKC force-pushed the tokio-des branch 2 times, most recently from 8a3799c to e2809d1 Compare July 8, 2026 14:35
@carlaKC

carlaKC commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

carlaKC and others added 5 commits July 8, 2026 10:42
Pin simln-lib/sim-cli to the virtual-time PR head (enabling simln-lib's
virtual-time feature) and adapt to the reimplemented SimulationClock:

- Construct the clock with the new SimulationClock::new(start_time) signature in
  place of the speedup multiplier, across the binaries, tests and graph setup.
- Reimplement the local InstantClock trait on tokio::time::Instant so elapsed
  time tracks virtual time.
- Pass the latency interceptor's new seed parameter (None for now; seeding
  follows in a later commit).

The binaries still build their own clock and run on a standard runtime; moving
them onto the paused virtual-time runtime happens next.

Tracks bitcoin-dev-project/sim-ln#309.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make each binary's main() synchronous and drive run() through
runtime::block_on_virtual_time, which owns the paused single-threaded runtime and
hands run() its clock. A run now advances virtual time straight to the next
event, completing as fast as the CPU allows instead of sleeping on real time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The virtual-time SimulationClock no longer takes a speedup multiplier, so the
--clock-speedup flag (and its default) is now dead. Drop it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the virtual-time runtime a run is deterministic for a fixed seed, so thread
one shared SIM_SEED through the simulation. Feed it to both the SimulationCfg
(payment generation, preimages) and the latency interceptor's seed parameter so
that sampled delays are reproducible too, not just payments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Virtual time has no wall-clock limit, so an attack that never triggers shutdown
would advance time forever. Cap the attack simulation's duration at one virtual
year as a safeguard; attacks are expected to terminate well before this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@carlaKC
carlaKC requested a review from elnosh July 8, 2026 14:52
@carlaKC
carlaKC merged commit ab2d8cb into master Jul 9, 2026
2 checks passed
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