From 37966bc6a224d736b758e5c688d7083d4770c634 Mon Sep 17 00:00:00 2001 From: nkatha23 Date: Tue, 24 Feb 2026 01:54:12 +0300 Subject: [PATCH 1/4] fix: allow onion addresses and external peers in messages command - Add is_external_peer() helper to detect onion addresses and non-tank peers - Skip namespace parsing and kubectl IP lookups for external peers - Match message capture directories directly by address for external peers - Fix dot-split validation that would reject onion addresses containing dots - Add check_messages() test to onion_test.py to verify the fix Fixes #761 --- src/warnet/bitcoin.py | 51 ++++++++++++++++++++++++++++++++----------- test/onion_test.py | 20 +++++++++++++++++ 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 5b392e22e..438c06031 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -151,6 +151,15 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): return matching_logs +@bitcoin.command() +@click.argument("tank_a", type=str, required=True) +@click.argument("tank_b", type=str, required=True) +@click.option("--chain", default="regtest", show_default=True) +def is_external_peer(name: str) -> bool: + """Check if tank_b is an external/non-tank peer (e.g. onion address or raw IP)""" + return ".onion" in name or not name.startswith("tank-") + + @bitcoin.command() @click.argument("tank_a", type=str, required=True) @click.argument("tank_b", type=str, required=True) @@ -159,6 +168,7 @@ def messages(tank_a: str, tank_b: str, chain: str): """ Fetch messages sent between and in [chain] + tank_b can be a tank pod name, an onion address, or any external peer identifier. Optionally, include a namespace like so: tank-name.namespace """ @@ -177,12 +187,26 @@ def parse_name_and_namespace(tank: str) -> tuple[str, Optional[str]]: click.secho(f"Foramts found: {tank_a} {tank_b}") sys.exit(1) + # Only validate tank_b format if it's not an external peer + if not is_external_peer(tank_b): + tank_b_split = tank_b.split(".") + if len(tank_b_split) > 2: + click.secho("Accepted formats: tank-name OR tank-name.namespace") + click.secho(f"Format found: {tank_b}") + sys.exit(1) + tank_a, namespace_a = parse_name_and_namespace(tank_a) - tank_b, namespace_b = parse_name_and_namespace(tank_b) + + # If tank_b is an external peer (onion address etc.), skip namespace parsing + if is_external_peer(tank_b): + namespace_b = None + else: + tank_b, namespace_b = parse_name_and_namespace(tank_b) try: namespace_a = get_default_namespace_or(namespace_a) - namespace_b = get_default_namespace_or(namespace_b) + if namespace_b is not None: + namespace_b = get_default_namespace_or(namespace_b) # Get the messages messages = get_messages( @@ -190,9 +214,8 @@ def parse_name_and_namespace(tank: str) -> tuple[str, Optional[str]]: ) if not messages: - print( - f"No messages found between {tank_a} ({namespace_a}) and {tank_b} ({namespace_b})" - ) + peer_display = tank_b if namespace_b is None else f"{tank_b} ({namespace_b})" + print(f"No messages found between {tank_a} ({namespace_a}) and {peer_display}") return # Process and print messages @@ -224,15 +247,17 @@ def get_messages(tank_a: str, tank_b: str, chain: str, namespace_a: str, namespa subdir = "" if chain == "main" else f"{chain}/" base_dir = f"/root/.bitcoin/{subdir}message_capture" - # Get the IP of node_b - cmd = f"kubectl get pod {tank_b} -o jsonpath='{{.status.podIP}}' --namespace {namespace_b}" - tank_b_ip = run_command(cmd).strip() + if namespace_b is None: + # External peer (onion address, raw IP, etc.) — use address directly for dir matching + tank_b_ip = tank_b + tank_b_service_ip = tank_b + else: + # Known tank — look up IPs via kubectl + cmd = f"kubectl get pod {tank_b} -o jsonpath='{{.status.podIP}}' --namespace {namespace_b}" + tank_b_ip = run_command(cmd).strip() - # Get the service IP of node_b - cmd = ( - f"kubectl get service {tank_b} -o jsonpath='{{.spec.clusterIP}}' --namespace {namespace_b}" - ) - tank_b_service_ip = run_command(cmd).strip() + cmd = f"kubectl get service {tank_b} -o jsonpath='{{.spec.clusterIP}}' --namespace {namespace_b}" + tank_b_service_ip = run_command(cmd).strip() # List directories in the message capture folder cmd = f"kubectl exec {tank_a} --namespace {namespace_a} -- ls {base_dir}" diff --git a/test/onion_test.py b/test/onion_test.py index 75ec6b049..713c72f4b 100755 --- a/test/onion_test.py +++ b/test/onion_test.py @@ -76,6 +76,26 @@ def onion_connect(): self.wait_for_predicate(onion_connect, timeout=20 * 60) + self.check_messages(onions) + + +def check_messages(self, onions: dict): + self.log.info("Checking captured messages from onion peer...") + + # tank-0001's onion address as seen by tank-0000 + onion_b = onions["tank-0001"] + + # Bitcoin message capture dirs are named like: _ or _ + # We pass the onion address with port appended as tank_b + onion_b_with_port = f"{onion_b}_18444" + + result = self.warnet(f"bitcoin messages tank-0000 {onion_b_with_port}") + self.log.info(f"Messages result: {result}") + assert result and len(result.strip()) > 0, ( + f"Expected messages between tank-0000 and {onion_b_with_port} but got none" + ) + self.log.info("Successfully retrieved messages from onion peer!") + if __name__ == "__main__": test = OnionTest() From 060e691f5e586c95c1ee3c42117a2b3bcda4c3e4 Mon Sep 17 00:00:00 2001 From: nkatha23 Date: Tue, 24 Feb 2026 02:10:23 +0300 Subject: [PATCH 2/4] fix: remove original dot-split validation that rejected onion addresses --- src/warnet/bitcoin.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 438c06031..986d7a1a4 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -151,10 +151,6 @@ def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): return matching_logs -@bitcoin.command() -@click.argument("tank_a", type=str, required=True) -@click.argument("tank_b", type=str, required=True) -@click.option("--chain", default="regtest", show_default=True) def is_external_peer(name: str) -> bool: """Check if tank_b is an external/non-tank peer (e.g. onion address or raw IP)""" return ".onion" in name or not name.startswith("tank-") @@ -181,8 +177,7 @@ def parse_name_and_namespace(tank: str) -> tuple[str, Optional[str]]: return tank_split[0], namespace tank_a_split = tank_a.split(".") - tank_b_split = tank_b.split(".") - if len(tank_a_split) > 2 or len(tank_b_split) > 2: + if len(tank_a_split) > 2: click.secho("Accepted formats: tank-name OR tank-name.namespace") click.secho(f"Foramts found: {tank_a} {tank_b}") sys.exit(1) From a68ba873fab0f95b614075bf3fbaeedfba8048a1 Mon Sep 17 00:00:00 2001 From: nkatha23 Date: Tue, 24 Feb 2026 17:01:04 +0300 Subject: [PATCH 3/4] fix: move check_messages inside OnionTest class --- test/onion_test.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/test/onion_test.py b/test/onion_test.py index 713c72f4b..b50cda486 100755 --- a/test/onion_test.py +++ b/test/onion_test.py @@ -78,23 +78,22 @@ def onion_connect(): self.check_messages(onions) - -def check_messages(self, onions: dict): - self.log.info("Checking captured messages from onion peer...") - - # tank-0001's onion address as seen by tank-0000 - onion_b = onions["tank-0001"] - - # Bitcoin message capture dirs are named like: _ or _ - # We pass the onion address with port appended as tank_b - onion_b_with_port = f"{onion_b}_18444" - - result = self.warnet(f"bitcoin messages tank-0000 {onion_b_with_port}") - self.log.info(f"Messages result: {result}") - assert result and len(result.strip()) > 0, ( - f"Expected messages between tank-0000 and {onion_b_with_port} but got none" - ) - self.log.info("Successfully retrieved messages from onion peer!") + def check_messages(self, onions: dict): + self.log.info("Checking captured messages from onion peer...") + + # tank-0001's onion address as seen by tank-0000 + onion_b = onions["tank-0001"] + + # Bitcoin message capture dirs are named like: _ or _ + # We pass the onion address with port appended as tank_b + onion_b_with_port = f"{onion_b}_18444" + + result = self.warnet(f"bitcoin messages tank-0000 {onion_b_with_port}") + self.log.info(f"Messages result: {result}") + assert result and len(result.strip()) > 0, ( + f"Expected messages between tank-0000 and {onion_b_with_port} but got none" + ) + self.log.info("Successfully retrieved messages from onion peer!") if __name__ == "__main__": From e13039e3c3afb563eeb1d60bba1f9eebb555a5c8 Mon Sep 17 00:00:00 2001 From: nkatha23 Date: Thu, 26 Feb 2026 16:09:00 +0300 Subject: [PATCH 4/4] fix: resolve namespace before get_messages to fix tank-to-tank message lookup The previous implementation used namespace_b = None as a sentinel for both 'known tank with no explicit namespace' and 'external/onion peer', causing known tanks like tank-0001 to fall into the external peer path in get_messages. This set tank_b_ip = 'tank-0001' instead of doing the kubectl IP lookup, so no message capture directories ever matched. Fix: resolve namespace_b via get_default_namespace_or() for known tanks before calling get_messages, so only genuine external peers arrive with namespace_b = None. Fixes rpc_test verack assertion failure introduced in previous commit. --- src/warnet/bitcoin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py index 986d7a1a4..8d6676cf2 100644 --- a/src/warnet/bitcoin.py +++ b/src/warnet/bitcoin.py @@ -197,11 +197,10 @@ def parse_name_and_namespace(tank: str) -> tuple[str, Optional[str]]: namespace_b = None else: tank_b, namespace_b = parse_name_and_namespace(tank_b) + namespace_b = get_default_namespace_or(namespace_b) try: namespace_a = get_default_namespace_or(namespace_a) - if namespace_b is not None: - namespace_b = get_default_namespace_or(namespace_b) # Get the messages messages = get_messages(