Skip to content

Commit 2b30a3c

Browse files
authored
Merge pull request #232 from chuksys/parsing-mod
refactor: move parsing into its own module
2 parents 68cb804 + 6b1b530 commit 2b30a3c

File tree

9 files changed

+368
-301
lines changed

9 files changed

+368
-301
lines changed

.github/workflows/build-and-test.yml

100644100755
+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: Install protoc
1414
run: sudo apt install -y protobuf-compiler
1515
- uses: actions/checkout@v4
16-
- uses: dtolnay/rust-toolchain@1.84.0
16+
- uses: dtolnay/rust-toolchain@1.85.0
1717
- name: Install Clippy
1818
run: rustup component add clippy
1919
- name: Install Rustfmt

Cargo.lock

100644100755
File mode changed.

sim-cli/Cargo.toml

100644100755
File mode changed.

sim-cli/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod parsing;

sim-cli/src/main.rs

100644100755
+2-259
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,7 @@
1-
use anyhow::anyhow;
2-
use bitcoin::secp256k1::PublicKey;
3-
use clap::builder::TypedValueParser;
41
use clap::Parser;
52
use log::LevelFilter;
6-
use simln_lib::{
7-
cln::ClnNode, eclair::EclairNode, lnd::LndNode, ActivityDefinition, LightningError,
8-
LightningNode, NodeConnection, NodeId, SimParams, Simulation, SimulationCfg, WriteResults,
9-
};
3+
use sim_cli::parsing::{create_simulation, Cli};
104
use simple_logger::SimpleLogger;
11-
use std::collections::HashMap;
12-
use std::path::PathBuf;
13-
use std::sync::Arc;
14-
use tokio::sync::Mutex;
15-
use tokio_util::task::TaskTracker;
16-
17-
/// The default directory where the simulation files are stored and where the results will be written to.
18-
pub const DEFAULT_DATA_DIR: &str = ".";
19-
20-
/// The default simulation file to be used by the simulator.
21-
pub const DEFAULT_SIM_FILE: &str = "sim.json";
22-
23-
/// The default expected payment amount for the simulation, around ~$10 at the time of writing.
24-
pub const DEFAULT_EXPECTED_PAYMENT_AMOUNT: u64 = 3_800_000;
25-
26-
/// The number of times over each node in the network sends its total deployed capacity in a calendar month.
27-
pub const DEFAULT_ACTIVITY_MULTIPLIER: f64 = 2.0;
28-
29-
/// Default batch size to flush result data to disk
30-
const DEFAULT_PRINT_BATCH_SIZE: u32 = 500;
31-
32-
/// Deserializes a f64 as long as it is positive and greater than 0.
33-
fn deserialize_f64_greater_than_zero(x: String) -> Result<f64, String> {
34-
match x.parse::<f64>() {
35-
Ok(x) => {
36-
if x > 0.0 {
37-
Ok(x)
38-
} else {
39-
Err(format!(
40-
"capacity_multiplier must be higher than 0. {x} received."
41-
))
42-
}
43-
},
44-
Err(e) => Err(e.to_string()),
45-
}
46-
}
47-
48-
#[derive(Parser)]
49-
#[command(version, about)]
50-
struct Cli {
51-
/// Path to a directory containing simulation files, and where simulation results will be stored
52-
#[clap(long, short, default_value = DEFAULT_DATA_DIR)]
53-
data_dir: PathBuf,
54-
/// Path to the simulation file to be used by the simulator
55-
/// This can either be an absolute path, or relative path with respect to data_dir
56-
#[clap(long, short, default_value = DEFAULT_SIM_FILE)]
57-
sim_file: PathBuf,
58-
/// Total time the simulator will be running
59-
#[clap(long, short)]
60-
total_time: Option<u32>,
61-
/// Number of activity results to batch together before printing to csv file [min: 1]
62-
#[clap(long, short, default_value_t = DEFAULT_PRINT_BATCH_SIZE, value_parser = clap::builder::RangedU64ValueParser::<u32>::new().range(1..u32::MAX as u64))]
63-
print_batch_size: u32,
64-
/// Level of verbosity of the messages displayed by the simulator.
65-
/// Possible values: [off, error, warn, info, debug, trace]
66-
#[clap(long, short, verbatim_doc_comment, default_value = "info")]
67-
log_level: LevelFilter,
68-
/// Expected payment amount for the random activity generator
69-
#[clap(long, short, default_value_t = DEFAULT_EXPECTED_PAYMENT_AMOUNT, value_parser = clap::builder::RangedU64ValueParser::<u64>::new().range(1..u64::MAX))]
70-
expected_pmt_amt: u64,
71-
/// Multiplier of the overall network capacity used by the random activity generator
72-
#[clap(long, short, default_value_t = DEFAULT_ACTIVITY_MULTIPLIER, value_parser = clap::builder::StringValueParser::new().try_map(deserialize_f64_greater_than_zero))]
73-
capacity_multiplier: f64,
74-
/// Do not create an output file containing the simulations results
75-
#[clap(long, default_value_t = false)]
76-
no_results: bool,
77-
/// Seed to run random activity generator deterministically
78-
#[clap(long, short)]
79-
fix_seed: Option<u64>,
80-
}
815

826
#[tokio::main]
837
async fn main() -> anyhow::Result<()> {
@@ -96,134 +20,7 @@ async fn main() -> anyhow::Result<()> {
9620
.init()
9721
.unwrap();
9822

99-
let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file).await?;
100-
let SimParams { nodes, activity } =
101-
serde_json::from_str(&std::fs::read_to_string(sim_path)?)
102-
.map_err(|e| anyhow!("Could not deserialize node connection data or activity description from simulation file (line {}, col {}).", e.line(), e.column()))?;
103-
104-
let mut clients: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>> = HashMap::new();
105-
let mut pk_node_map = HashMap::new();
106-
let mut alias_node_map = HashMap::new();
107-
108-
for connection in nodes {
109-
// TODO: Feels like there should be a better way of doing this without having to Arc<Mutex<T>>> it at this time.
110-
// Box sort of works, but we won't know the size of the dyn LightningNode at compile time so the compiler will
111-
// scream at us when trying to create the Arc<Mutex>> later on while adding the node to the clients map
112-
let node: Arc<Mutex<dyn LightningNode>> = match connection {
113-
NodeConnection::LND(c) => Arc::new(Mutex::new(LndNode::new(c).await?)),
114-
NodeConnection::CLN(c) => Arc::new(Mutex::new(ClnNode::new(c).await?)),
115-
NodeConnection::ECLAIR(c) => Arc::new(Mutex::new(EclairNode::new(c).await?)),
116-
};
117-
118-
let node_info = node.lock().await.get_info().clone();
119-
120-
log::info!(
121-
"Connected to {} - Node ID: {}.",
122-
node_info.alias,
123-
node_info.pubkey
124-
);
125-
126-
if clients.contains_key(&node_info.pubkey) {
127-
anyhow::bail!(LightningError::ValidationError(format!(
128-
"duplicated node: {}.",
129-
node_info.pubkey
130-
)));
131-
}
132-
133-
if alias_node_map.contains_key(&node_info.alias) {
134-
anyhow::bail!(LightningError::ValidationError(format!(
135-
"duplicated node: {}.",
136-
node_info.alias
137-
)));
138-
}
139-
140-
clients.insert(node_info.pubkey, node);
141-
pk_node_map.insert(node_info.pubkey, node_info.clone());
142-
alias_node_map.insert(node_info.alias.clone(), node_info);
143-
}
144-
145-
let mut validated_activities = vec![];
146-
// Make all the activities identifiable by PK internally
147-
for act in activity.into_iter() {
148-
// We can only map aliases to nodes we control, so if either the source or destination alias
149-
// is not in alias_node_map, we fail
150-
let source = if let Some(source) = match &act.source {
151-
NodeId::PublicKey(pk) => pk_node_map.get(pk),
152-
NodeId::Alias(a) => alias_node_map.get(a),
153-
} {
154-
source.clone()
155-
} else {
156-
anyhow::bail!(LightningError::ValidationError(format!(
157-
"activity source {} not found in nodes.",
158-
act.source
159-
)));
160-
};
161-
162-
let destination = match &act.destination {
163-
NodeId::Alias(a) => {
164-
if let Some(info) = alias_node_map.get(a) {
165-
info.clone()
166-
} else {
167-
anyhow::bail!(LightningError::ValidationError(format!(
168-
"unknown activity destination: {}.",
169-
act.destination
170-
)));
171-
}
172-
},
173-
NodeId::PublicKey(pk) => {
174-
if let Some(info) = pk_node_map.get(pk) {
175-
info.clone()
176-
} else {
177-
clients
178-
.get(&source.pubkey)
179-
.unwrap()
180-
.lock()
181-
.await
182-
.get_node_info(pk)
183-
.await
184-
.map_err(|e| {
185-
log::debug!("{}", e);
186-
LightningError::ValidationError(format!(
187-
"Destination node unknown or invalid: {}.",
188-
pk,
189-
))
190-
})?
191-
}
192-
},
193-
};
194-
195-
validated_activities.push(ActivityDefinition {
196-
source,
197-
destination,
198-
start_secs: act.start_secs,
199-
count: act.count,
200-
interval_secs: act.interval_secs,
201-
amount_msat: act.amount_msat,
202-
});
203-
}
204-
205-
let write_results = if !cli.no_results {
206-
Some(WriteResults {
207-
results_dir: mkdir(cli.data_dir.join("results")).await?,
208-
batch_size: cli.print_batch_size,
209-
})
210-
} else {
211-
None
212-
};
213-
214-
let tasks = TaskTracker::new();
215-
let sim = Simulation::new(
216-
SimulationCfg::new(
217-
cli.total_time,
218-
cli.expected_pmt_amt,
219-
cli.capacity_multiplier,
220-
write_results,
221-
cli.fix_seed,
222-
),
223-
clients,
224-
validated_activities,
225-
tasks,
226-
);
23+
let sim = create_simulation(&cli).await?;
22724
let sim2 = sim.clone();
22825

22926
ctrlc::set_handler(move || {
@@ -235,57 +32,3 @@ async fn main() -> anyhow::Result<()> {
23532

23633
Ok(())
23734
}
238-
239-
async fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result<PathBuf> {
240-
if sim_file.exists() {
241-
Ok(sim_file)
242-
} else if sim_file.is_relative() {
243-
let sim_path = data_dir.join(sim_file);
244-
if sim_path.exists() {
245-
Ok(sim_path)
246-
} else {
247-
log::info!("Simulation file '{}' does not exist.", sim_path.display());
248-
select_sim_file(data_dir).await
249-
}
250-
} else {
251-
log::info!("Simulation file '{}' does not exist.", sim_file.display());
252-
select_sim_file(data_dir).await
253-
}
254-
}
255-
256-
async fn select_sim_file(data_dir: PathBuf) -> anyhow::Result<PathBuf> {
257-
let sim_files = std::fs::read_dir(data_dir.clone())?
258-
.filter_map(|f| {
259-
f.ok().and_then(|f| {
260-
if f.path().extension()?.to_str()? == "json" {
261-
f.file_name().into_string().ok()
262-
} else {
263-
None
264-
}
265-
})
266-
})
267-
.collect::<Vec<_>>();
268-
269-
if sim_files.is_empty() {
270-
anyhow::bail!(
271-
"no simulation files found in {}.",
272-
data_dir.canonicalize()?.display()
273-
);
274-
}
275-
276-
let selection = dialoguer::Select::new()
277-
.with_prompt(format!(
278-
"Select a simulation file. Found these in {}",
279-
data_dir.canonicalize()?.display()
280-
))
281-
.items(&sim_files)
282-
.default(0)
283-
.interact()?;
284-
285-
Ok(data_dir.join(sim_files[selection].clone()))
286-
}
287-
288-
async fn mkdir(dir: PathBuf) -> anyhow::Result<PathBuf> {
289-
tokio::fs::create_dir_all(&dir).await?;
290-
Ok(dir)
291-
}

0 commit comments

Comments
 (0)