fix: harden DGR-003 identity trust boundary
This commit is contained in:
@@ -45,7 +45,7 @@ import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass, field, replace
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -60,6 +60,7 @@ from .capability import (
|
||||
STATE_ADMITTED,
|
||||
STATE_MODEL_MISMATCH,
|
||||
STATE_SHARD_MISMATCH,
|
||||
STATE_UNCERTIFIED,
|
||||
CapabilityState,
|
||||
absent_state,
|
||||
evaluate_report,
|
||||
@@ -84,7 +85,13 @@ from .routing_stats import (
|
||||
)
|
||||
from .model_files import files_for_layer_range, snapshot_dir_for_repo
|
||||
from .raft import RaftNode
|
||||
from .recipe import CertificationLedger
|
||||
from .recipe import (
|
||||
CertificationLedger,
|
||||
DistributedForwardEvidence,
|
||||
PresentedIdentity,
|
||||
RecipeIdentityError,
|
||||
RecipeStatus,
|
||||
)
|
||||
|
||||
|
||||
_CONSOLE_LIMIT = 300
|
||||
@@ -833,6 +840,11 @@ def _admitted_nodes(nodes: list["_NodeEntry"], policy: str | None) -> list["_Nod
|
||||
return [node for node in nodes if _capability_routable(node, effective)]
|
||||
|
||||
|
||||
def _route_identity_partition(node: "_NodeEntry") -> tuple[str, ...]:
|
||||
fingerprint = _node_admission(node).fingerprint
|
||||
return ("legacy",) if fingerprint is None else ("exact", *fingerprint)
|
||||
|
||||
|
||||
def _select_route(
|
||||
nodes: list[_NodeEntry],
|
||||
required_start: int,
|
||||
@@ -856,29 +868,51 @@ def _select_route(
|
||||
],
|
||||
key=lambda n: (n.shard_start, -n.shard_end), # type: ignore[operator]
|
||||
)
|
||||
route: list[_NodeEntry] = []
|
||||
covered_up_to = required_start - 1
|
||||
|
||||
def _routing_score(node: "_NodeEntry") -> float:
|
||||
return _effective_throughput(node, model) * _reputation_multiplier(node, contracts)
|
||||
|
||||
while covered_up_to < required_end:
|
||||
best: _NodeEntry | None = None
|
||||
for node in candidates:
|
||||
if node.shard_start <= covered_up_to + 1 and node.shard_end > covered_up_to:
|
||||
if best is None:
|
||||
best = node
|
||||
elif node.shard_end > best.shard_end:
|
||||
best = node
|
||||
elif node.shard_end == best.shard_end and _routing_score(node) > _routing_score(best):
|
||||
best = node
|
||||
if best is None:
|
||||
missing = covered_up_to + 1
|
||||
return [], f"no route available: no registered node covers layer {missing}"
|
||||
route.append(best)
|
||||
covered_up_to = best.shard_end
|
||||
candidates = [n for n in candidates if n is not best]
|
||||
partitions: dict[tuple[str, ...], list[_NodeEntry]] = {}
|
||||
for node in candidates:
|
||||
partitions.setdefault(_route_identity_partition(node), []).append(node)
|
||||
|
||||
complete: list[list[_NodeEntry]] = []
|
||||
furthest = required_start - 1
|
||||
for partition in partitions.values():
|
||||
pool = list(partition)
|
||||
route: list[_NodeEntry] = []
|
||||
covered_up_to = required_start - 1
|
||||
while covered_up_to < required_end:
|
||||
best: _NodeEntry | None = None
|
||||
for node in pool:
|
||||
if node.shard_start <= covered_up_to + 1 and node.shard_end > covered_up_to:
|
||||
if best is None:
|
||||
best = node
|
||||
elif node.shard_end > best.shard_end:
|
||||
best = node
|
||||
elif (
|
||||
node.shard_end == best.shard_end
|
||||
and _routing_score(node) > _routing_score(best)
|
||||
):
|
||||
best = node
|
||||
if best is None:
|
||||
break
|
||||
route.append(best)
|
||||
covered_up_to = best.shard_end
|
||||
pool = [node for node in pool if node is not best]
|
||||
furthest = max(furthest, covered_up_to)
|
||||
if covered_up_to >= required_end:
|
||||
complete.append(route)
|
||||
|
||||
if not complete:
|
||||
return [], f"no route available: no registered node covers layer {furthest + 1}"
|
||||
route = max(
|
||||
complete,
|
||||
key=lambda candidate: (
|
||||
min(_routing_score(node) for node in candidate),
|
||||
-len(candidate),
|
||||
),
|
||||
)
|
||||
return route, ""
|
||||
|
||||
|
||||
@@ -914,7 +948,12 @@ def _enumerate_routes(
|
||||
for head in heads:
|
||||
route = [head]
|
||||
covered_up_to = head.shard_end
|
||||
pool = [n for n in sharded if n is not head]
|
||||
head_partition = _route_identity_partition(head)
|
||||
pool = [
|
||||
node
|
||||
for node in sharded
|
||||
if node is not head and _route_identity_partition(node) == head_partition
|
||||
]
|
||||
while covered_up_to < required_end:
|
||||
best = None
|
||||
for n in pool:
|
||||
@@ -2059,14 +2098,24 @@ def _find_pinned_route(
|
||||
hop_count: int,
|
||||
) -> list[_NodeEntry] | None:
|
||||
"""First combination of exactly ``hop_count`` distinct nodes covering the
|
||||
layer range, where every node extends coverage (US-030 benchmark routes)."""
|
||||
layer range, where every node extends coverage (US-030 benchmark routes).
|
||||
|
||||
Benchmark combos run real inference, so they obey the same DGR-003 rule as
|
||||
every other route builder: one route, one exact identity. A combo that mixed
|
||||
two fingerprints — or an exact Shard with a legacy one — would measure a
|
||||
numerically incoherent route and record the garbage as a benchmark.
|
||||
"""
|
||||
for combo in itertools.permutations(nodes, hop_count):
|
||||
covered = required_start - 1
|
||||
valid = True
|
||||
partition = _route_identity_partition(combo[0])
|
||||
for candidate in combo:
|
||||
if candidate.shard_start is None or candidate.shard_end is None:
|
||||
valid = False
|
||||
break
|
||||
if _route_identity_partition(candidate) != partition:
|
||||
valid = False
|
||||
break
|
||||
if candidate.shard_start > covered + 1 or candidate.shard_end <= covered:
|
||||
valid = False
|
||||
break
|
||||
@@ -6647,6 +6696,84 @@ class TrackerServer:
|
||||
self._test_runner: TestRunManager | None = test_runner
|
||||
self.port: int | None = None
|
||||
|
||||
def certify_recipe(
|
||||
self,
|
||||
identity: PresentedIdentity,
|
||||
evidence: DistributedForwardEvidence,
|
||||
) -> RecipeStatus:
|
||||
"""Promote one exact recipe from tracker-recorded distributed evidence.
|
||||
|
||||
This is the tracker's only certification authority. The evidence names
|
||||
the nodes that served the forward; this method refuses to take any of
|
||||
them on the evidence's word. For each one it goes back to what *this
|
||||
tracker* re-derived at admission and requires the evidence to describe
|
||||
the same Shard: same route fingerprint, same registered range, and the
|
||||
same shard binding digest — the derivative's own bytes and exact range.
|
||||
|
||||
Without the binding check, a route fingerprint is range-independent by
|
||||
design (:mod:`meshnet_tracker.recipe`), so evidence could name the right
|
||||
recipe while describing splits nobody on the route was actually serving,
|
||||
and the certification would attach to blobs that never ran.
|
||||
"""
|
||||
with self._lock:
|
||||
for node_id, participant in zip(
|
||||
evidence.node_ids, evidence.participants, strict=True
|
||||
):
|
||||
node = self._registry.get(node_id)
|
||||
if node is None:
|
||||
raise RecipeIdentityError(
|
||||
f"certification participant {node_id!r} is not registered"
|
||||
)
|
||||
admitted = node.capability
|
||||
if admitted.fingerprint != identity.key:
|
||||
raise RecipeIdentityError(
|
||||
f"certification participant {node_id!r} is not admitted under "
|
||||
"the promoted fingerprint"
|
||||
)
|
||||
if admitted.state not in (STATE_UNCERTIFIED, STATE_ADMITTED):
|
||||
raise RecipeIdentityError(
|
||||
f"certification participant {node_id!r} has capability state "
|
||||
f"{admitted.state!r}"
|
||||
)
|
||||
registered_range = (
|
||||
node.shard_start,
|
||||
None if node.shard_end is None else node.shard_end + 1,
|
||||
)
|
||||
if registered_range != (
|
||||
participant.shard_start,
|
||||
participant.shard_end,
|
||||
):
|
||||
raise RecipeIdentityError(
|
||||
f"certification participant {node_id!r} range differs from its "
|
||||
"registered Shard range"
|
||||
)
|
||||
# The exact bytes and range this node was admitted on. A node that
|
||||
# registered without an identity has no binding, and cannot be a
|
||||
# participant in an exact certification at all.
|
||||
if admitted.shard_binding_digest is None:
|
||||
raise RecipeIdentityError(
|
||||
f"certification participant {node_id!r} registered no exact "
|
||||
"Shard binding; it cannot certify a recipe"
|
||||
)
|
||||
if admitted.shard_binding_digest != participant.shard_binding_digest:
|
||||
raise RecipeIdentityError(
|
||||
f"certification participant {node_id!r} presents a Shard "
|
||||
"binding this tracker did not admit it on; the evidence "
|
||||
"describes different derivative bytes or a different range"
|
||||
)
|
||||
|
||||
status = self._recipe_certifications.certify(identity, evidence)
|
||||
for node in self._registry.values():
|
||||
if (
|
||||
node.capability.state == STATE_UNCERTIFIED
|
||||
and node.capability.fingerprint == identity.key
|
||||
):
|
||||
node.capability = replace(
|
||||
node.capability.with_state(STATE_ADMITTED, status.detail),
|
||||
certification=status.status,
|
||||
)
|
||||
return status
|
||||
|
||||
def _start_embedded_relay(self) -> dict:
|
||||
"""Start the shared RelayServer class in-process for tracker+relay deployments."""
|
||||
if not self._embedded_relay_enabled:
|
||||
|
||||
Reference in New Issue
Block a user