Skip to content

Commit

Permalink
Merge pull request bitcoin-dev-project#282 from willcl-ark/ensure-gra…
Browse files Browse the repository at this point in the history
…ph-connections
  • Loading branch information
willcl-ark authored Feb 21, 2024
2 parents 9599d4b + 802f388 commit 3095326
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 52 deletions.
8 changes: 4 additions & 4 deletions src/cli/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ def graph():


@graph.command()
@click.argument("params", nargs=-1, type=str)
@click.argument("number", type=int)
@click.option("--outfile", type=Path)
@click.option("--version", type=str, default=DEFAULT_TAG)
@click.option("--bitcoin_conf", type=Path)
@click.option("--random", is_flag=True)
def create(
params: list[str], outfile: Path, version: str, bitcoin_conf: Path, random: bool = False
number: int, outfile: Path, version: str, bitcoin_conf: Path, random: bool = False
):
"""
Create a graph file of type random AS graph with [params]
Create a cycle graph with [n] nodes, and additionally include 7 extra random outbounds per node.
Returns XML file as string with or without --outfile option
"""
print(
rpc_call(
"graph_generate",
{
"params": params,
"n": number,
"outfile": str(outfile) if outfile else "",
"version": version,
"bitcoin_conf": str(bitcoin_conf),
Expand Down
15 changes: 5 additions & 10 deletions src/warnet/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from flask_jsonrpc.app import JSONRPC
from flask_jsonrpc.exceptions import ServerError
from warnet.utils import (
create_graph_with_probability,
create_cycle_graph,
gen_config_dir,
)
from warnet.warnet import Warnet
Expand Down Expand Up @@ -412,26 +412,21 @@ def thread_start(wn, lock: threading.Lock):

def graph_generate(
self,
params: list[str],
n: int,
outfile: str,
version: str,
bitcoin_conf: str | None = None,
random: bool = False,
) -> str:
try:
graph_func = nx.generators.erdos_renyi_graph
# Default connectivity probability of 0.2
if not any(param.startswith("p=") for param in params):
params.append("p=0.2")

graph = create_graph_with_probability(graph_func, params, version, bitcoin_conf, random)
graph = create_cycle_graph(n, version, bitcoin_conf, random)

if outfile:
file_path = Path(outfile)
nx.write_graphml(graph, file_path)
nx.write_graphml(graph, file_path, named_key_ids=True)
return f"Generated graph written to file: {outfile}"
bio = BytesIO()
nx.write_graphml(graph, bio)
nx.write_graphml(graph, bio, named_key_ids=True)
xml_data = bio.getvalue()
return xml_data.decode('utf-8')
except Exception as e:
Expand Down
56 changes: 20 additions & 36 deletions src/warnet/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,54 +421,31 @@ def default_bitcoin_conf_args() -> str:
return " ".join(conf_args)


def create_graph_with_probability(
graph_func, params: list, version: str, bitcoin_conf: str | None, random_version: bool
def create_cycle_graph(
n: int, version: str, bitcoin_conf: str | None, random_version: bool
):
kwargs = {}
for param in params:
try:
key, value = param.split("=")
kwargs[key] = value
except ValueError:
msg = f"Invalid parameter format: {param}"
logger.error(msg)
return msg

# Attempt to convert numerical values from string to their respective numerical types
msg = "Error convering numerical value strings to types "
for key in kwargs:
try:
kwargs[key] = int(kwargs[key])
except ValueError:
try:
kwargs[key] = float(kwargs[key])
except ValueError as e:
msg += str(e)
return msg
except Exception as e:
msg += str(e)
return msg

logger.debug(f"Parsed params: {kwargs}")

try:
graph = graph_func(**kwargs)
# Use nx.DiGraph() as base otherwise edges not always made in specific directions
graph = nx.generators.cycle_graph(n, nx.DiGraph())
except TypeError as e:
msg = f"Failed to create graph: {e}"
logger.error(msg)
return msg

# Ensure each node has at least 8 edges
# Graph is a simply cycle graph with all nodes connected in a loop, including both ends.
# Ensure each node has at least 8 outbound connections by making 7 more outbound connections
for node in graph.nodes():
while graph.degree(node) < 8:
logger.debug(f"Creating additional connections for node {node}")
for _ in range(8):
# Choose a random node to connect to
# Make sure it's not the same node and they aren't already connected
potential_nodes = [
n for n in range(kwargs["n"]) if n != node and not graph.has_edge(node, n)
]
potential_nodes = [ node for node in range(n) if n != node and not graph.has_edge(node, n) ]
if potential_nodes:
chosen_node = random.choice(potential_nodes)
graph.add_edge(node, chosen_node)
logger.debug(f"Added edge: {node}:{chosen_node}")
logger.debug(f"Node {node} edges: {graph.edges(node)}")

# calculate degree
degree_dict = dict(graph.degree(graph.nodes()))
Expand All @@ -491,13 +468,20 @@ def create_graph_with_probability(
conf_contents = dump_bitcoin_conf(conf_dict, for_graph=True)

# populate our custom fields
for node in graph.nodes():
for i, node in enumerate(graph.nodes()):
if random_version:
graph.nodes[node]["version"] = random.choice(WEIGHTED_TAGS)
else:
graph.nodes[node]["version"] = version
# One node demoing the image tag
if i == 1:
graph.nodes[node]["image"] = f"bitcoindevproject/bitcoin:{version}"
else:
graph.nodes[node]["version"] = version
graph.nodes[node]["bitcoin_config"] = conf_contents
graph.nodes[node]["tc_netem"] = ""
graph.nodes[node]["build_args"] = ""
graph.nodes[node]["exporter"] = ""
graph.nodes[node]["collect_logs"] = ""

convert_unsupported_attributes(graph)
return graph
Expand Down
4 changes: 2 additions & 2 deletions test/graph_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
tf = f"{dir}/{str(uuid.uuid4())}.graphml"
if base.backend == "compose":
print(f"Server writing test graph directly to {tf}")
print(base.warcli(f"graph create n=10 --outfile={tf} --version={DEFAULT_TAG}", network=False))
print(base.warcli(f"graph create 10 --outfile={tf} --version={DEFAULT_TAG}", network=False))
base.wait_for_predicate(lambda: Path(tf).exists())
else:
print(f"Client retrieving test graph from RPC and writing to {tf}")
xml = base.warcli(f"graph create n=10 --version={DEFAULT_TAG}", network=False)
xml = base.warcli(f"graph create 10 --version={DEFAULT_TAG}", network=False)
print(xml)
with open(tf, "w") as file:
file.write(xml)
Expand Down

0 comments on commit 3095326

Please sign in to comment.