From 8a751f8d4589b104ea247f338846a92a2cc8b51c Mon Sep 17 00:00:00 2001 From: Alessandro Date: Thu, 5 Mar 2026 10:07:50 +0100 Subject: [PATCH 1/6] feat: add attestation registry submission in redmesh close flow --- .../cybersec/red_mesh/pentester_api_01.py | 167 +++++++++++++++++- 1 file changed, 164 insertions(+), 3 deletions(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index 68e8ddd6..d16014dd 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -30,8 +30,11 @@ """ +import ipaddress import random +from urllib.parse import urlparse + from naeural_core.business.default.web_app.fast_api_web_app import FastApiWebAppPlugin as BasePlugin from .redmesh_utils import PentestLocalWorker # Import PentestJob from separate module from .redmesh_llm_agent_mixin import _RedMeshLlmAgentMixin @@ -47,7 +50,7 @@ RISK_CRED_PENALTY_CAP, ) -__VER__ = '0.8.2' +__VER__ = '0.9.0' _CONFIG = { **BasePlugin.CONFIG, @@ -94,6 +97,10 @@ "SCANNER_IDENTITY": "probe.redmesh.local", # EHLO domain for SMTP probes "SCANNER_USER_AGENT": "", # HTTP User-Agent (empty = default requests UA) + # RedMesh attestation submission + "ATTESTATION_ENABLED": True, + "ATTESTATION_MIN_SECONDS_BETWEEN_SUBMITS": 86400, + 'VALIDATION_RULES': { **BasePlugin.CONFIG['VALIDATION_RULES'], }, @@ -120,6 +127,7 @@ class PentesterApi01Plugin(BasePlugin, _RedMeshLlmAgentMixin): List of job_ids that completed locally (used for status responses). """ CONFIG = _CONFIG + REDMESH_ATTESTATION_DOMAIN = "0xced141225d43c56d8b224d12f0b9524a15dc86df0113c42ffa4bc859309e0d40" def on_init(self): @@ -211,6 +219,129 @@ def Pd(self, s, *args, score=-1, **kwargs): return + def _attestation_get_tenant_private_key(self): + env_name = "R1EN_ATTESTATION_PRIVATE_KEY" + private_key = self.os_environ.get(env_name, None) + if private_key: + private_key = private_key.strip() + if not private_key: + return None + return private_key + + @staticmethod + def _attestation_pack_cid_obfuscated(report_cid) -> str: + if not isinstance(report_cid, str) or len(report_cid.strip()) == 0: + return "0x" + ("00" * 10) + cid = report_cid.strip() + if len(cid) >= 10: + masked = cid[:5] + cid[-5:] + else: + masked = cid.ljust(10, "_") + safe = "".join(ch if 32 <= ord(ch) <= 126 else "_" for ch in masked)[:10] + data = safe.encode("ascii", errors="ignore") + if len(data) < 10: + data = data + (b"_" * (10 - len(data))) + return "0x" + data[:10].hex() + + @staticmethod + def _attestation_extract_host(target): + if not isinstance(target, str): + return None + target = target.strip() + if not target: + return None + if "://" in target: + parsed = urlparse(target) + if parsed.hostname: + return parsed.hostname + host = target.split("/", 1)[0] + if host.count(":") == 1 and "." in host: + host = host.split(":", 1)[0] + return host + + def _attestation_pack_ip_obfuscated(self, target) -> str: + host = self._attestation_extract_host(target) + if not host: + return "0x0000" + if ".." in host: + parts = host.split("..") + if len(parts) == 2 and all(part.isdigit() for part in parts): + first_octet = int(parts[0]) + last_octet = int(parts[1]) + if 0 <= first_octet <= 255 and 0 <= last_octet <= 255: + return f"0x{first_octet:02x}{last_octet:02x}" + try: + ip_obj = ipaddress.ip_address(host) + except Exception: + return "0x0000" + if ip_obj.version != 4: + return "0x0000" + octets = host.split(".") + first_octet = int(octets[0]) + last_octet = int(octets[-1]) + return f"0x{first_octet:02x}{last_octet:02x}" + + + def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulnerability_score): + if not self.cfg_attestation_enabled: + return None + tenant_private_key = self._attestation_get_tenant_private_key() + if tenant_private_key is None: + self.P( + "RedMesh attestation is enabled but tenant private key is missing. " + "Expected env var 'R1EN_ATTESTATION_PRIVATE_KEY'.", + color='y' + ) + return None + + run_mode = str(job_specs.get("run_mode", "SINGLEPASS")).upper() + test_mode = 1 if run_mode == "CONTINUOUS_MONITORING" else 0 + node_count = len(workers) if isinstance(workers, dict) else 0 + # TODO: replace placeholder score with proper RedMesh vulnerability scoring logic. + target = job_specs.get("target") + report_cid = workers.get(self.ee_addr, {}).get("report_cid", None) #TODO: use the correct CID + node_eth_address = self.bc.eth_address + ip_obfuscated = self._attestation_pack_ip_obfuscated(target) + cid_obfuscated = self._attestation_pack_cid_obfuscated(report_cid) + + tx_hash = self.bc.submit_attestation( + function_name="submitRedmeshAttestation", + function_args=[ + test_mode, + node_count, + vulnerability_score, + ip_obfuscated, + cid_obfuscated, + ], + signature_types=["bytes32", "uint8", "uint16", "uint8", "bytes2", "bytes10"], + signature_values=[ + self.REDMESH_ATTESTATION_DOMAIN, + test_mode, + node_count, + vulnerability_score, + ip_obfuscated, + cid_obfuscated, + ], + tx_private_key=tenant_private_key, + ) + + result = { + "job_id": job_id, + "tx_hash": tx_hash, + "test_mode": "C" if test_mode == 1 else "S", + "node_count": node_count, + "vulnerability_score": vulnerability_score, + "report_cid": report_cid, + "node_eth_address": node_eth_address, + } + self.P( + "Submitted RedMesh attestation for " + f"{job_id} (tx: {tx_hash}, node: {node_eth_address}, score: {vulnerability_score})", + color='g' + ) + return result + + def __post_init(self): """ Perform warmup: reconcile existing jobs in CStore, migrate legacy keys, @@ -1107,23 +1238,53 @@ def _maybe_finalize_pass(self): # ═══════════════════════════════════════════════════ pass_date_started = self._get_timeline_date(job_specs, "pass_started") or self._get_timeline_date(job_specs, "created") pass_date_completed = self.time() - pass_history.append({ + pass_record = ({ "pass_nr": job_pass, "date_started": pass_date_started, "date_completed": pass_date_completed, "duration": round(pass_date_completed - pass_date_started, 2) if pass_date_started else None, "reports": {addr: w.get("report_cid") for addr, w in workers.items()} }) + now_ts = self.time() # Compute risk score for this pass aggregated_for_score = self._collect_aggregated_report(workers) + risk_score = 0 if aggregated_for_score: risk_result = self._compute_risk_score(aggregated_for_score) pass_history[-1]["risk_score"] = risk_result["score"] pass_history[-1]["risk_breakdown"] = risk_result["breakdown"] - job_specs["risk_score"] = risk_result["score"] + risk_score = risk_result["score"] + job_specs["risk_score"] = risk_score self.P(f"Risk score for job {job_id} pass {job_pass}: {risk_result['score']}/100") + should_submit_attestation = True + if run_mode == "CONTINUOUS_MONITORING": + last_attestation_at = job_specs.get("last_attestation_at") + min_interval = self.cfg_attestation_min_seconds_between_submits + if last_attestation_at is not None and now_ts - last_attestation_at < min_interval: + should_submit_attestation = False + + if should_submit_attestation: + # Best-effort on-chain summary; failures must not block pass finalization. + try: + redmesh_attestation = self._submit_attestation( + job_id=job_id, + job_specs=job_specs, + workers=workers, + vulnerability_score=risk_score + ) + if redmesh_attestation is not None: + pass_record["redmesh_attestation"] = redmesh_attestation + job_specs["last_attestation_at"] = now_ts + except Exception as exc: + self.P( + f"Failed to submit RedMesh attestation for job {job_id}: {exc}", + color='r' + ) + + pass_history.append(pass_record) + # Handle SINGLEPASS - set FINALIZED and exit (no scheduling) if run_mode == "SINGLEPASS": job_specs["job_status"] = "FINALIZED" From 49ba4921a18be5c72c689419f0813f4ed86d9a1b Mon Sep 17 00:00:00 2001 From: Alessandro Date: Thu, 5 Mar 2026 10:27:30 +0100 Subject: [PATCH 2/6] fix: add execution_id to attestation --- .../cybersec/red_mesh/pentester_api_01.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index d16014dd..b2398292 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -281,6 +281,19 @@ def _attestation_pack_ip_obfuscated(self, target) -> str: last_octet = int(octets[-1]) return f"0x{first_octet:02x}{last_octet:02x}" + @staticmethod + def _attestation_pack_execution_id(job_id) -> str: + if not isinstance(job_id, str): + raise ValueError("job_id must be a string") + job_id = job_id.strip() + if len(job_id) != 8: + raise ValueError("job_id must be exactly 8 characters") + try: + data = job_id.encode("ascii") + except UnicodeEncodeError as exc: + raise ValueError("job_id must contain only ASCII characters") from exc + return "0x" + data.hex() + def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulnerability_score): if not self.cfg_attestation_enabled: @@ -299,6 +312,7 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulne node_count = len(workers) if isinstance(workers, dict) else 0 # TODO: replace placeholder score with proper RedMesh vulnerability scoring logic. target = job_specs.get("target") + execution_id = self._attestation_pack_execution_id(job_id) report_cid = workers.get(self.ee_addr, {}).get("report_cid", None) #TODO: use the correct CID node_eth_address = self.bc.eth_address ip_obfuscated = self._attestation_pack_ip_obfuscated(target) @@ -310,15 +324,17 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulne test_mode, node_count, vulnerability_score, + execution_id, ip_obfuscated, cid_obfuscated, ], - signature_types=["bytes32", "uint8", "uint16", "uint8", "bytes2", "bytes10"], + signature_types=["bytes32", "uint8", "uint16", "uint8", "bytes8", "bytes2", "bytes10"], signature_values=[ self.REDMESH_ATTESTATION_DOMAIN, test_mode, node_count, vulnerability_score, + execution_id, ip_obfuscated, cid_obfuscated, ], @@ -331,6 +347,7 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulne "test_mode": "C" if test_mode == 1 else "S", "node_count": node_count, "vulnerability_score": vulnerability_score, + "execution_id": execution_id, "report_cid": report_cid, "node_eth_address": node_eth_address, } From 49470ba67f442480c37b86227dd64228ef7e76cd Mon Sep 17 00:00:00 2001 From: Alessandro Date: Thu, 5 Mar 2026 11:43:50 +0100 Subject: [PATCH 3/6] Add RedMesh job-start attestation submission flow --- .../cybersec/red_mesh/pentester_api_01.py | 109 ++++++++++++++++-- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index b2398292..09953ce0 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -295,7 +295,28 @@ def _attestation_pack_execution_id(job_id) -> str: return "0x" + data.hex() - def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulnerability_score): + def _attestation_get_worker_eth_addresses(self, workers: dict) -> list[str]: + if not isinstance(workers, dict): + return [] + eth_addresses = [] + for node_addr in workers.keys(): + eth_addr = self.bc.node_addr_to_eth_addr(node_addr) + if not isinstance(eth_addr, str) or not eth_addr.startswith("0x"): + raise ValueError(f"Unable to convert worker node to EVM address: {node_addr}") + eth_addresses.append(eth_addr) + eth_addresses.sort() + return eth_addresses + + def _attestation_pack_node_hashes(self, workers: dict) -> str: + eth_addresses = self._attestation_get_worker_eth_addresses(workers) + if len(eth_addresses) == 0: + return "0x" + ("00" * 32) + digest = self.bc.eth_hash_message(types=["address[]"], values=[eth_addresses], as_hex=True) + if isinstance(digest, str) and digest.startswith("0x"): + return digest + return "0x" + str(digest) + + def _submit_redmesh_test_attestation(self, job_id: str, job_specs: dict, workers: dict, vulnerability_score=0): if not self.cfg_attestation_enabled: return None tenant_private_key = self._attestation_get_tenant_private_key() @@ -319,7 +340,7 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulne cid_obfuscated = self._attestation_pack_cid_obfuscated(report_cid) tx_hash = self.bc.submit_attestation( - function_name="submitRedmeshAttestation", + function_name="submitRedmeshTestAttestation", function_args=[ test_mode, node_count, @@ -352,12 +373,71 @@ def _submit_attestation(self, job_id: str, job_specs: dict, workers: dict, vulne "node_eth_address": node_eth_address, } self.P( - "Submitted RedMesh attestation for " + "Submitted RedMesh test attestation for " f"{job_id} (tx: {tx_hash}, node: {node_eth_address}, score: {vulnerability_score})", color='g' ) return result + def _submit_redmesh_job_start_attestation(self, job_id: str, job_specs: dict, workers: dict): + if not self.cfg_attestation_enabled: + return None + tenant_private_key = self._attestation_get_tenant_private_key() + if tenant_private_key is None: + self.P( + "RedMesh attestation is enabled but tenant private key is missing. " + "Expected env var 'R1EN_ATTESTATION_PRIVATE_KEY'.", + color='y' + ) + return None + + run_mode = str(job_specs.get("run_mode", "SINGLEPASS")).upper() + test_mode = 1 if run_mode == "CONTINUOUS_MONITORING" else 0 + node_count = len(workers) if isinstance(workers, dict) else 0 + target = job_specs.get("target") + execution_id = self._attestation_pack_execution_id(job_id) + node_eth_address = self.bc.eth_address + ip_obfuscated = self._attestation_pack_ip_obfuscated(target) + node_hashes = self._attestation_pack_node_hashes(workers) + + tx_hash = self.bc.submit_attestation( + function_name="submitRedmeshJobStartAttestation", + function_args=[ + test_mode, + node_count, + execution_id, + node_hashes, + ip_obfuscated, + ], + signature_types=["bytes32", "uint8", "uint16", "bytes8", "bytes32", "bytes2"], + signature_values=[ + self.REDMESH_ATTESTATION_DOMAIN, + test_mode, + node_count, + execution_id, + node_hashes, + ip_obfuscated, + ], + tx_private_key=tenant_private_key, + ) + + result = { + "job_id": job_id, + "tx_hash": tx_hash, + "test_mode": "C" if test_mode == 1 else "S", + "node_count": node_count, + "execution_id": execution_id, + "node_hashes": node_hashes, + "ip_obfuscated": ip_obfuscated, + "node_eth_address": node_eth_address, + } + self.P( + "Submitted RedMesh job-start attestation for " + f"{job_id} (tx: {tx_hash}, node: {node_eth_address}, node_count: {node_count})", + color='g' + ) + return result + def __post_init(self): """ @@ -1285,18 +1365,18 @@ def _maybe_finalize_pass(self): if should_submit_attestation: # Best-effort on-chain summary; failures must not block pass finalization. try: - redmesh_attestation = self._submit_attestation( + redmesh_test_attestation = self._submit_redmesh_test_attestation( job_id=job_id, job_specs=job_specs, workers=workers, vulnerability_score=risk_score ) - if redmesh_attestation is not None: - pass_record["redmesh_attestation"] = redmesh_attestation + if redmesh_test_attestation is not None: + pass_record["redmesh_test_attestation"] = redmesh_test_attestation job_specs["last_attestation_at"] = now_ts except Exception as exc: self.P( - f"Failed to submit RedMesh attestation for job {job_id}: {exc}", + f"Failed to submit RedMesh test attestation for job {job_id}: {exc}", color='r' ) @@ -1772,6 +1852,21 @@ def launch_test( actor_type="user" ) self._emit_timeline_event(job_specs, "started", "Scan started", actor=self.ee_id, actor_type="node") + + try: + redmesh_job_start_attestation = self._submit_redmesh_job_start_attestation( + job_id=job_id, + job_specs=job_specs, + workers=workers, + ) + if redmesh_job_start_attestation is not None: + job_specs["redmesh_job_start_attestation"] = redmesh_job_start_attestation + except Exception as exc: + self.P( + f"Failed to submit RedMesh job-start attestation for job {job_id}: {exc}", + color='r' + ) + self.chainstore_hset( hkey=self.cfg_instance_id, key=job_id, From cec6ccba9380a570d87c304bfd489480b81e1eea Mon Sep 17 00:00:00 2001 From: toderian Date: Fri, 6 Mar 2026 15:08:42 +0000 Subject: [PATCH 4/6] fix: set up private key in plugin config --- extensions/business/cybersec/red_mesh/pentester_api_01.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index 09953ce0..6262c825 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -98,6 +98,7 @@ "SCANNER_USER_AGENT": "", # HTTP User-Agent (empty = default requests UA) # RedMesh attestation submission + "ATTESTATION_PRIVATE_KEY": "", "ATTESTATION_ENABLED": True, "ATTESTATION_MIN_SECONDS_BETWEEN_SUBMITS": 86400, @@ -220,8 +221,7 @@ def Pd(self, s, *args, score=-1, **kwargs): def _attestation_get_tenant_private_key(self): - env_name = "R1EN_ATTESTATION_PRIVATE_KEY" - private_key = self.os_environ.get(env_name, None) + private_key = self.cfg_attestation_private_key if private_key: private_key = private_key.strip() if not private_key: From df6d71eba03c398a71f4848c5ad319ebfa781ffb Mon Sep 17 00:00:00 2001 From: toderian Date: Fri, 6 Mar 2026 17:00:22 +0000 Subject: [PATCH 5/6] fix: pass history read --- extensions/business/cybersec/red_mesh/pentester_api_01.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index 6262c825..9a3f386f 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -1349,8 +1349,8 @@ def _maybe_finalize_pass(self): risk_score = 0 if aggregated_for_score: risk_result = self._compute_risk_score(aggregated_for_score) - pass_history[-1]["risk_score"] = risk_result["score"] - pass_history[-1]["risk_breakdown"] = risk_result["breakdown"] + pass_record["risk_score"] = risk_result["score"] + pass_record["risk_breakdown"] = risk_result["breakdown"] risk_score = risk_result["score"] job_specs["risk_score"] = risk_score self.P(f"Risk score for job {job_id} pass {job_pass}: {risk_result['score']}/100") From 3f2116945027d8506d1e39d578937f0b87cc604d Mon Sep 17 00:00:00 2001 From: toderian Date: Fri, 6 Mar 2026 19:04:04 +0000 Subject: [PATCH 6/6] fix: add loggign for attestation --- .../cybersec/red_mesh/pentester_api_01.py | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index 9a3f386f..078261e8 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -317,13 +317,15 @@ def _attestation_pack_node_hashes(self, workers: dict) -> str: return "0x" + str(digest) def _submit_redmesh_test_attestation(self, job_id: str, job_specs: dict, workers: dict, vulnerability_score=0): + self.P(f"[ATTESTATION] Test attestation requested for job {job_id} (score={vulnerability_score})") if not self.cfg_attestation_enabled: + self.P("[ATTESTATION] Attestation is disabled via config. Skipping.", color='y') return None tenant_private_key = self._attestation_get_tenant_private_key() if tenant_private_key is None: self.P( - "RedMesh attestation is enabled but tenant private key is missing. " - "Expected env var 'R1EN_ATTESTATION_PRIVATE_KEY'.", + "[ATTESTATION] Tenant private key is missing. " + "Expected env var 'R1EN_ATTESTATION_PRIVATE_KEY'. Skipping.", color='y' ) return None @@ -331,7 +333,6 @@ def _submit_redmesh_test_attestation(self, job_id: str, job_specs: dict, workers run_mode = str(job_specs.get("run_mode", "SINGLEPASS")).upper() test_mode = 1 if run_mode == "CONTINUOUS_MONITORING" else 0 node_count = len(workers) if isinstance(workers, dict) else 0 - # TODO: replace placeholder score with proper RedMesh vulnerability scoring logic. target = job_specs.get("target") execution_id = self._attestation_pack_execution_id(job_id) report_cid = workers.get(self.ee_addr, {}).get("report_cid", None) #TODO: use the correct CID @@ -339,6 +340,11 @@ def _submit_redmesh_test_attestation(self, job_id: str, job_specs: dict, workers ip_obfuscated = self._attestation_pack_ip_obfuscated(target) cid_obfuscated = self._attestation_pack_cid_obfuscated(report_cid) + self.P( + f"[ATTESTATION] Submitting test attestation: job={job_id}, mode={'CONTINUOUS' if test_mode else 'SINGLEPASS'}, " + f"nodes={node_count}, score={vulnerability_score}, target={ip_obfuscated}, " + f"cid={cid_obfuscated}, sender={node_eth_address}" + ) tx_hash = self.bc.submit_attestation( function_name="submitRedmeshTestAttestation", function_args=[ @@ -380,13 +386,15 @@ def _submit_redmesh_test_attestation(self, job_id: str, job_specs: dict, workers return result def _submit_redmesh_job_start_attestation(self, job_id: str, job_specs: dict, workers: dict): + self.P(f"[ATTESTATION] Job-start attestation requested for job {job_id}") if not self.cfg_attestation_enabled: + self.P("[ATTESTATION] Attestation is disabled via config. Skipping.", color='y') return None tenant_private_key = self._attestation_get_tenant_private_key() if tenant_private_key is None: self.P( - "RedMesh attestation is enabled but tenant private key is missing. " - "Expected env var 'R1EN_ATTESTATION_PRIVATE_KEY'.", + "[ATTESTATION] Tenant private key is missing. " + "Expected env var 'R1EN_ATTESTATION_PRIVATE_KEY'. Skipping.", color='y' ) return None @@ -400,6 +408,12 @@ def _submit_redmesh_job_start_attestation(self, job_id: str, job_specs: dict, wo ip_obfuscated = self._attestation_pack_ip_obfuscated(target) node_hashes = self._attestation_pack_node_hashes(workers) + worker_addrs = list(workers.keys()) if isinstance(workers, dict) else [] + self.P( + f"[ATTESTATION] Submitting job-start attestation: job={job_id}, mode={'CONTINUOUS' if test_mode else 'SINGLEPASS'}, " + f"nodes={node_count}, target={ip_obfuscated}, node_hashes={node_hashes}, " + f"workers={worker_addrs}, sender={node_eth_address}" + ) tx_hash = self.bc.submit_attestation( function_name="submitRedmeshJobStartAttestation", function_args=[ @@ -1360,6 +1374,12 @@ def _maybe_finalize_pass(self): last_attestation_at = job_specs.get("last_attestation_at") min_interval = self.cfg_attestation_min_seconds_between_submits if last_attestation_at is not None and now_ts - last_attestation_at < min_interval: + elapsed = round(now_ts - last_attestation_at) + self.P( + f"[ATTESTATION] Skipping test attestation for job {job_id}: " + f"last submitted {elapsed}s ago, min interval is {min_interval}s", + color='y' + ) should_submit_attestation = False if should_submit_attestation: @@ -1375,8 +1395,12 @@ def _maybe_finalize_pass(self): pass_record["redmesh_test_attestation"] = redmesh_test_attestation job_specs["last_attestation_at"] = now_ts except Exception as exc: + import traceback self.P( - f"Failed to submit RedMesh test attestation for job {job_id}: {exc}", + f"[ATTESTATION] Failed to submit test attestation for job {job_id}: {exc}\n" + f" Type: {type(exc).__name__}\n" + f" Args: {exc.args}\n" + f" Traceback:\n{traceback.format_exc()}", color='r' ) @@ -1862,8 +1886,12 @@ def launch_test( if redmesh_job_start_attestation is not None: job_specs["redmesh_job_start_attestation"] = redmesh_job_start_attestation except Exception as exc: + import traceback self.P( - f"Failed to submit RedMesh job-start attestation for job {job_id}: {exc}", + f"[ATTESTATION] Failed to submit job-start attestation for job {job_id}: {exc}\n" + f" Type: {type(exc).__name__}\n" + f" Args: {exc.args}\n" + f" Traceback:\n{traceback.format_exc()}", color='r' )