Skip to content

Commit b2126dd

Browse files
committed
graph: add generator
delete random AS graphs of size `n` using either default tag (latest) or random tags from SUPPORTED_TAGS
1 parent 417a6a7 commit b2126dd

File tree

4 files changed

+164
-2
lines changed

4 files changed

+164
-2
lines changed

src/warnet/cli/graph.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from pathlib import Path
2+
from typing import List
3+
4+
import click
5+
from rich import print
6+
7+
from warnet.cli.rpc import rpc_call
8+
from warnet.utils import DEFAULT_TAG
9+
10+
11+
@click.group(name="graph")
12+
def graph():
13+
"""Graph commands"""
14+
15+
16+
@graph.command()
17+
@click.argument("params", nargs=-1, type=str)
18+
@click.option("--outfile", type=Path)
19+
@click.option("--version", type=str, default=DEFAULT_TAG)
20+
@click.option("--bitcoin_conf", type=Path)
21+
@click.option("--random", is_flag=True)
22+
def create(params: List[str], outfile: Path, version: str, bitcoin_conf: Path, random: bool = False):
23+
"""
24+
Create a graph file of type random AS graph with [params]
25+
"""
26+
try:
27+
result = rpc_call(
28+
"graph_generate",
29+
{
30+
"params": params,
31+
"outfile": str(outfile) if outfile else "",
32+
"version": version,
33+
"bitcoin_conf": str(bitcoin_conf),
34+
"random": random
35+
},
36+
)
37+
print(result)
38+
except Exception as e:
39+
print(f"Error generating graph: {e}")

src/warnet/cli/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from rich import print as richprint
33

44
from warnet.cli.debug import debug
5+
from warnet.cli.graph import graph
56
from warnet.cli.network import network
67
from warnet.cli.rpc import rpc_call
78
from warnet.cli.scenarios import scenarios
@@ -12,6 +13,7 @@ def cli():
1213

1314

1415
cli.add_command(debug)
16+
cli.add_command(graph)
1517
cli.add_command(scenarios)
1618
cli.add_command(network)
1719

src/warnet/server.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@
66
import sys
77
import threading
88
from datetime import datetime
9+
from io import BytesIO
910
from logging.handlers import RotatingFileHandler
1011
from logging import StreamHandler
11-
from typing import List, Dict
12+
from pathlib import Path
13+
from typing import List, Dict, Optional
1214
from flask import Flask, request
1315
from flask_jsonrpc.app import JSONRPC
1416

17+
import networkx as nx
18+
1519
import scenarios
1620
from warnet.warnet import Warnet
1721
from warnet.utils import (
22+
create_graph_with_probability,
1823
gen_config_dir,
1924
)
2025

@@ -81,6 +86,8 @@ def setup_rpc(self):
8186
self.jsonrpc.register(self.network_down)
8287
self.jsonrpc.register(self.network_info)
8388
self.jsonrpc.register(self.network_status)
89+
# Graph
90+
self.jsonrpc.register(self.graph_generate)
8491
# Debug
8592
self.jsonrpc.register(self.generate_deployment)
8693
# Server
@@ -271,6 +278,20 @@ def thread_start(wn):
271278
t.start()
272279
return f"Starting warnet network named '{network}' with the following parameters:\n{wn}"
273280

281+
def graph_generate(self, params: List[str], outfile: str, version: str, bitcoin_conf: Optional[str] = None, random: bool = False) -> str:
282+
graph_func = nx.generators.random_internet_as_graph
283+
284+
graph = create_graph_with_probability(graph_func, params, version, bitcoin_conf, random)
285+
286+
if outfile:
287+
file_path = Path(outfile)
288+
nx.write_graphml(graph, file_path)
289+
return f"Generated graph written to file: {outfile}"
290+
bio = BytesIO()
291+
nx.write_graphml(graph, bio)
292+
xml_data = bio.getvalue()
293+
return f"Generated graph:\n\n{xml_data.decode('utf-8')}"
294+
274295
def network_down(self, network: str = "warnet") -> str:
275296
"""
276297
Stop all containers in <network>.

src/warnet/utils.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,25 @@
1111
import time
1212
from io import BytesIO
1313
from pathlib import Path
14+
from typing import Dict, List, Optional
15+
16+
import networkx as nx
1417

1518
from test_framework.p2p import MESSAGEMAP
1619
from test_framework.messages import ser_uint256
1720

1821

1922
logger = logging.getLogger("utils")
2023

24+
2125
SUPPORTED_TAGS = [
2226
"25.1",
2327
"24.2",
2428
"23.2",
2529
"22.2",
2630
]
31+
DEFAULT_TAG = SUPPORTED_TAGS[0]
32+
WEIGHTED_TAGS = [tag for index, tag in enumerate(reversed(SUPPORTED_TAGS)) for _ in range(index + 1)]
2733

2834

2935
def exponential_backoff(max_retries=5, base_delay=1, max_delay=32):
@@ -187,7 +193,7 @@ def parse_bitcoin_conf(file_content):
187193
return result
188194

189195

190-
def dump_bitcoin_conf(conf_dict):
196+
def dump_bitcoin_conf(conf_dict, for_graph=False):
191197
"""
192198
Converts a dictionary representation of bitcoin.conf content back to INI-style string.
193199
@@ -213,6 +219,9 @@ def dump_bitcoin_conf(conf_dict):
213219
for sub_key, sub_value in values:
214220
result.append(f"{sub_key}={sub_value}")
215221

222+
if for_graph:
223+
return ",".join(result)
224+
216225
# Terminate file with newline
217226
return "\n".join(result) + "\n"
218227

@@ -408,3 +417,94 @@ def default_bitcoin_conf_args() -> str:
408417
conf_args.append(f"-{key}={value}")
409418

410419
return " ".join(conf_args)
420+
421+
422+
def create_graph_with_probability(graph_func, params: List, version: str, bitcoin_conf: Optional[str], random_version: bool):
423+
kwargs = {}
424+
for param in params:
425+
try:
426+
key, value = param.split("=")
427+
kwargs[key] = value
428+
except ValueError:
429+
msg = f"Invalid parameter format: {param}"
430+
logger.error(msg)
431+
return msg
432+
433+
# Attempt to convert numerical values from string to their respective numerical types
434+
for key in kwargs:
435+
try:
436+
kwargs[key] = int(kwargs[key])
437+
except ValueError:
438+
try:
439+
kwargs[key] = float(kwargs[key])
440+
except ValueError:
441+
pass
442+
443+
logger.debug(f"Parsed params: {kwargs}")
444+
445+
try:
446+
graph = graph_func(**kwargs)
447+
except TypeError as e:
448+
msg = f"Failed to create graph: {e}"
449+
logger.error(msg)
450+
return msg
451+
452+
# calculate degree
453+
degree_dict = dict(graph.degree(graph.nodes()))
454+
nx.set_node_attributes(graph, degree_dict, 'degree')
455+
456+
# add a default layout
457+
pos = nx.spring_layout(graph)
458+
for node in graph.nodes():
459+
graph.nodes[node]['x'] = float(pos[node][0])
460+
graph.nodes[node]['y'] = float(pos[node][1])
461+
462+
# parse and process conf file
463+
conf_contents = ""
464+
if bitcoin_conf is not None:
465+
conf = Path(bitcoin_conf)
466+
if conf.is_file():
467+
with open(conf, 'r') as f:
468+
# parse INI style conf then dump using for_graph
469+
conf_dict = parse_bitcoin_conf(f.read())
470+
conf_contents = dump_bitcoin_conf(conf_dict, for_graph=True)
471+
472+
# populate our custom fields
473+
for node in graph.nodes():
474+
if random_version:
475+
graph.nodes[node]['version'] = random.choice(WEIGHTED_TAGS)
476+
else:
477+
graph.nodes[node]['version'] = version
478+
graph.nodes[node]['bitcoin_config'] = conf_contents
479+
graph.nodes[node]['tc_netem'] = ""
480+
481+
# remove type and customer fields from edges as we don't need 'em!
482+
for edge in graph.edges():
483+
del graph.edges[edge]["customer"]
484+
del graph.edges[edge]["type"]
485+
486+
convert_unsupported_attributes(graph)
487+
return graph
488+
489+
490+
def convert_unsupported_attributes(graph):
491+
# Sometimes networkx complains about invalid types when writing the graph
492+
# (it just generated itself!). Try to convert them here just in case.
493+
for _, node_data in graph.nodes(data=True):
494+
for key, value in node_data.items():
495+
if isinstance(value, set):
496+
node_data[key] = list(value)
497+
elif isinstance(value, (int, float, str)):
498+
continue
499+
else:
500+
node_data[key] = str(value)
501+
502+
for _, _, edge_data in graph.edges(data=True):
503+
for key, value in edge_data.items():
504+
if isinstance(value, set):
505+
edge_data[key] = list(value)
506+
elif isinstance(value, (int, float, str)):
507+
continue
508+
else:
509+
edge_data[key] = str(value)
510+

0 commit comments

Comments
 (0)