From ed2d66306fbbc0cf505c5531f81f9cd1de111460 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 20 Nov 2024 17:30:27 +0000 Subject: [PATCH 1/1] Initial checkin --- Cargo.toml | 11 +++ src/internal.rs | 201 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 58 ++++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/internal.rs create mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..50abd8c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ln-routing-replay" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitcoin = "0.32" +# Note that we have to turn on no-std to disable the timestamp range verification on channel_update +lightning = { version = "0.0.125", default-features = false, features = ["no-std"] } diff --git a/src/internal.rs b/src/internal.rs new file mode 100644 index 0000000..d54b4ab --- /dev/null +++ b/src/internal.rs @@ -0,0 +1,201 @@ +use crate::{do_setup, process_probe_result, results_complete, DirectedChannel, ProbeResult}; + +use bitcoin::constants::ChainHash; +use bitcoin::network::Network; +use bitcoin::hex::FromHex; +use bitcoin::TxOut; + +use lightning::ln::msgs::{ChannelAnnouncement, ChannelUpdate}; +use lightning::routing::gossip::{NetworkGraph, NodeId, ReadOnlyNetworkGraph}; +use lightning::routing::utxo::{UtxoLookup, UtxoResult}; +use lightning::util::logger::{Logger, Record}; +use lightning::util::ser::Readable; + +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::str::FromStr; + +struct DevNullLogger; +impl Logger for DevNullLogger { + fn log(&self, record: Record) { + #[cfg(debug_assertions)] + eprintln!("{}", record.args); + } +} + +fn open_file(f: &'static str) -> impl Iterator { + match File::open(f) { + Ok(file) => BufReader::new(file).lines().into_iter().map(move |res| match res { + Ok(line) => line, + Err(e) => { + eprintln!("Failed to read line from file {}: {:?}", f, e); + std::process::exit(1); + }, + }), + Err(e) => { + eprintln!("Failed to open {}, please fetch from https://bitcoin.ninja/ln-routing-replay/{}: {:?}", f, f, e); + std::process::exit(1); + } + } +} + +#[derive(Debug)] +struct ParsedChannel { + scid: u64, + amount: u64, +} + +#[derive(Debug)] +struct ParsedProbeResult { + timestamp: u64, + starting_node: NodeId, + passed_chans: Vec, + failed_chan: Option, +} + +impl ParsedProbeResult { + fn into_pub(self, graph: ReadOnlyNetworkGraph) -> ProbeResult { + let mut channels_with_sufficient_liquidity = Vec::new(); + let mut src_node_id = self.starting_node; + for hop in self.passed_chans { + channels_with_sufficient_liquidity.push(DirectedChannel { + src_node_id, + short_channel_id: hop.scid, + amount_msat: hop.amount, + }); + let chan = graph.channels().get(&hop.scid) + .expect("Missing channel in probe results"); + src_node_id = + if chan.node_one == src_node_id { chan.node_two } else { chan.node_one }; + } + let channel_that_rejected_payment = self.failed_chan.map(|hop| + DirectedChannel { + src_node_id, + short_channel_id: hop.scid, + amount_msat: hop.amount, + } + ); + ProbeResult { + timestamp: self.timestamp, + channels_with_sufficient_liquidity, + channel_that_rejected_payment, + } + } +} + +fn parse_probe(line: String) -> Option { + macro_rules! dbg_unw { ($val: expr) => { { let v = $val; debug_assert!(v.is_some()); v? } } } + let timestamp_string = dbg_unw!(line.split_once(' ')).0; + let timestamp = dbg_unw!(timestamp_string.parse::().ok()); + let useful_out = dbg_unw!(line.split_once(" - start at ")).1; + let (src_node, path_string) = dbg_unw!(useful_out.split_once(" ")); + let mut passed_chans = Vec::new(); + let mut failed_chan = None; + for hop in path_string.split(',') { + let (_ldk_prob, fields) = dbg_unw!(hop.split_once('(')); + let mut fields = fields.split(' '); + let amount_string = dbg_unw!(fields.next()); + let amount = dbg_unw!(amount_string.parse::().ok()); + if dbg_unw!(fields.next()) != "msat" { debug_assert!(false); return None; } + if dbg_unw!(fields.next()) != "on" { debug_assert!(false); return None; } + let mut scid_string = dbg_unw!(fields.next()); + scid_string = scid_string.strip_suffix(')').unwrap_or(scid_string); + let scid = dbg_unw!(scid_string.parse::().ok()); + let chan = ParsedChannel { + scid, + amount, + }; + if hop.starts_with("inv ") { + if failed_chan.is_some() { debug_assert!(false); return None; } + failed_chan = Some(chan); + } else { + passed_chans.push(chan); + } + } + Some(ParsedProbeResult { + timestamp, + starting_node: dbg_unw!(NodeId::from_str(src_node).ok()), + passed_chans, + failed_chan, + }) +} + +fn parse_update(line: String) -> Option<(u64, ChannelUpdate)> { + let (timestamp, update_hex) = line.split_once('|').expect("Invalid ChannelUpdate line"); + let update_bytes = Vec::::from_hex(update_hex).ok().expect("Invalid ChannelUpdate hex"); + let update = Readable::read(&mut &update_bytes[..]); + debug_assert!(update.is_ok()); + Some((timestamp.parse().ok()?, update.ok()?)) +} + +fn parse_announcement(line: String) -> Option<(u64, ChannelAnnouncement)> { + let (timestamp, ann_hex) = line.split_once('|').expect("Invalid ChannelAnnouncement line"); + let ann_bytes = Vec::::from_hex(ann_hex).ok().expect("Invalid ChannelAnnouncement hex"); + let announcement = Readable::read(&mut &ann_bytes[..]); + debug_assert!(announcement.is_ok()); + Some((timestamp.parse().ok()?, announcement.ok()?)) +} + +struct FundingValueProvider { + channel_values: HashMap, +} + +impl UtxoLookup for FundingValueProvider { + fn get_utxo(&self, _: &ChainHash, scid: u64) -> UtxoResult { + UtxoResult::Sync(Ok( + self.channel_values.get(&scid).expect("Missing Channel Value").clone() + )) + } +} + +pub fn main() { + let graph = NetworkGraph::new(Network::Bitcoin, &DevNullLogger); + let mut updates = open_file("channel_updates.txt").filter_map(parse_update).peekable(); + let mut announcements = open_file("channel_announcements.txt").filter_map(parse_announcement).peekable(); + let mut probe_results = open_file("probes.txt").filter_map(parse_probe).peekable(); + + let channel_values = HashMap::new(); + let utxo_values = FundingValueProvider { channel_values }; + let mut utxo_lookup = Some(&utxo_values); +utxo_lookup = None; + + let mut state = do_setup(); + + loop { + let next_update = updates.peek(); + let next_announcement = announcements.peek(); + let next_probe_result = probe_results.peek(); + + if next_update.is_none() && next_announcement.is_none() && next_probe_result.is_none() { + break; + } + + let next_update_ts = next_update.map(|(t, _)| *t).unwrap_or(u64::MAX); + let next_announce_ts = next_announcement.map(|(t, _)| *t).unwrap_or(u64::MAX); + let next_probe_ts = next_probe_result.map(|res| res.timestamp).unwrap_or(u64::MAX); + match (next_update_ts < next_announce_ts, next_announce_ts < next_probe_ts, next_update_ts < next_probe_ts) { + (true, _, true) => { + if let Some((_, update)) = updates.next() { + let res = graph.update_channel_unsigned(&update.contents); + if let Err(e) = res { + debug_assert_eq!(e.err, "Update older than last processed update"); + } + } else { unreachable!() } + } + (false, true, _) => { + if let Some((_, announcement)) = announcements.next() { + graph.update_channel_from_unsigned_announcement(&announcement.contents, &utxo_lookup) + .expect("announcements should be valid"); + } else { unreachable!() } + } + _ => { + if let Some(res) = probe_results.next() { + process_probe_result(graph.read_only(), res.into_pub(graph.read_only()), &mut state); + } else { unreachable!() } + } + } + } + + results_complete(state); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a62dc2d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,58 @@ +mod internal; + +use lightning::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; + +/// The simulation state. +/// +/// You're free to put whatever you want here. +pub struct State { +} + +/// Creates a new [`State`] before any probe results are processed. +pub fn do_setup() -> State { + State {} +} + +/// Processes one probe result. +/// +/// The network graph as it existed at `result.timestamp` is provided, as well as a reference to +/// the current state. +pub fn process_probe_result(network_graph: ReadOnlyNetworkGraph, result: ProbeResult, state: &mut State) { + +} + +/// This is run after all probe results have been processed, and should be used for printing +/// results or any required teardown. +pub fn results_complete(state: State) { + +} + +/// A hop in a route, consisting of a channel and the source public key, as well as the amount +/// which we were (or were not) able to send over the channel. +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct DirectedChannel { + /// The source node of a channel + pub src_node_id: NodeId, + /// The SCID of the channel + pub short_channel_id: u64, + /// The amount which may (or may not) have been sendable over this channel + pub amount_msat: u64, +} + +/// The result of a probe through the lightning network. +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct ProbeResult { + /// The time at which the probe was completed (i.e. we got the result), as a UNIX timestamp. + pub timestamp: u64, + /// Channels over which the probe *succeeded*. + /// + /// Note that these skip the first hop of the payment, as the first hop is not something which + /// needs to be predicted (we know our own local channel balances). + pub channels_with_sufficient_liquidity: Vec, + /// The channel at which the probe failed, if any + pub channel_that_rejected_payment: Option, +} + +fn main() { + internal::main(); +} -- 2.39.5