]> git.bitcoin.ninja Git - ln-routing-replay/commitdiff
Initial checkin
authorMatt Corallo <git@bluematt.me>
Wed, 20 Nov 2024 17:30:27 +0000 (17:30 +0000)
committerMatt Corallo <git@bluematt.me>
Wed, 20 Nov 2024 17:30:27 +0000 (17:30 +0000)
Cargo.toml [new file with mode: 0644]
src/internal.rs [new file with mode: 0644]
src/main.rs [new file with mode: 0644]

diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..50abd8c
--- /dev/null
@@ -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 (file)
index 0000000..d54b4ab
--- /dev/null
@@ -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<Item = String> {
+       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<ParsedChannel>,
+       failed_chan: Option<ParsedChannel>,
+}
+
+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<ParsedProbeResult> {
+       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::<u64>().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::<u64>().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::<u64>().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::<u8>::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::<u8>::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<u64, TxOut>,
+}
+
+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 (file)
index 0000000..a62dc2d
--- /dev/null
@@ -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<DirectedChannel>,
+       /// The channel at which the probe failed, if any
+       pub channel_that_rejected_payment: Option<DirectedChannel>,
+}
+
+fn main() {
+       internal::main();
+}