fix: harden DGR-003 identity trust boundary
This commit is contained in:
@@ -38,16 +38,17 @@ a recipe does not change a single number, so a rename must not partition a
|
||||
route; changing `kv_dtype` changes every number, so it must. The digest commits
|
||||
to what changes the numbers.
|
||||
|
||||
Recipes are **registered-but-dark** on arrival: known, visible to an operator,
|
||||
and not routable for user traffic. A recipe leaves the dark only when a *real
|
||||
distributed forward* over a route of at least two distinct physical nodes,
|
||||
covering the whole model, has emitted real tokens — see
|
||||
:class:`DistributedForwardEvidence`. Detected hardware is not a capability, a
|
||||
single-host forward is not a distributed forward, and a synthetic worker
|
||||
certifies nothing.
|
||||
**Certification is not here.** Recipes are registered-but-dark on arrival, and a
|
||||
recipe leaves the dark only when a real distributed forward over at least two
|
||||
distinct physical nodes has emitted real tokens. That ledger lives in the
|
||||
Tracker (:class:`meshnet_tracker.recipe.CertificationLedger`) and nowhere else —
|
||||
a node that could decide it was certified would be marking its own homework, and
|
||||
a second copy of the policy here would be a second thing to keep in step with it.
|
||||
This module builds and compares identity; the Tracker decides what may serve.
|
||||
|
||||
The tracker re-derives all of this independently in
|
||||
``meshnet_tracker.recipe`` — it does not import this package. The two
|
||||
The tracker re-derives the digests independently in ``meshnet_tracker.recipe`` —
|
||||
it does not import this package, because an admission gate that shares an
|
||||
implementation with the thing it admits is not an independent check. The two
|
||||
implementations are pinned together by a committed conformance vector
|
||||
(``tests/data/recipe_fingerprint_vectors.json``); if they ever drift, a test
|
||||
fails rather than a route silently forming.
|
||||
@@ -58,8 +59,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Iterable, Mapping, Sequence
|
||||
|
||||
from .native_protocol import BUNDLE_VERSION, SCHEMA_VERSION, pb
|
||||
@@ -72,6 +72,7 @@ RECIPE_IDENTITY_SCHEMA_VERSION = 1
|
||||
# with — a recipe digest, even if their canonical JSON were somehow identical.
|
||||
ARTIFACT_DIGEST_DOMAIN = "meshnet.model-artifact.v1"
|
||||
RECIPE_DIGEST_DOMAIN = "meshnet.runtime-recipe.v1"
|
||||
SHARD_BINDING_DIGEST_DOMAIN = "meshnet.shard-binding.v1"
|
||||
|
||||
# The axes of a runtime recipe. Every one of these changes the numbers a Shard
|
||||
# produces, so every one of them is part of identity and none of them may be
|
||||
@@ -102,7 +103,8 @@ MISMATCH_RUNTIME = "runtime"
|
||||
MISMATCH_BOUNDARY_SCHEMA = "boundary-schema"
|
||||
MISMATCH_RANGE = "range"
|
||||
MISMATCH_FINGERPRINT = "fingerprint"
|
||||
MISMATCH_UNCERTIFIED = "uncertified"
|
||||
MISMATCH_ROUTE_SESSION = "route-session"
|
||||
MISMATCH_ROUTE_EPOCH = "route-epoch"
|
||||
|
||||
# Which fail-closed reason each axis reports. Several axes share a reason
|
||||
# because they fail for the same operational cause: `activation_dtype` and
|
||||
@@ -122,14 +124,6 @@ _AXIS_MISMATCH: Mapping[str, str] = {
|
||||
"protocol_schema_version": MISMATCH_BOUNDARY_SCHEMA,
|
||||
}
|
||||
|
||||
# Certification states. `dark` is the arrival state, not an error state.
|
||||
STATUS_UNKNOWN = "unknown"
|
||||
STATUS_DARK = "dark"
|
||||
STATUS_CERTIFIED = "certified"
|
||||
|
||||
# A route that proves a recipe must be a real distributed route.
|
||||
MIN_CERTIFYING_NODES = 2
|
||||
|
||||
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
# A revision that can move is not a pin. DGR-017 learned this on the artifact;
|
||||
@@ -141,10 +135,6 @@ class RecipeIdentityError(ValueError):
|
||||
"""Malformed identity input. Messages name the field, never echo a payload."""
|
||||
|
||||
|
||||
class RecipeNotCertified(RecipeIdentityError):
|
||||
"""A recipe was asked to serve user traffic while still dark."""
|
||||
|
||||
|
||||
class RouteIncompatible(RecipeIdentityError):
|
||||
"""Two Shards cannot form an Inference Route.
|
||||
|
||||
@@ -618,6 +608,39 @@ class ShardIdentity:
|
||||
catalogue_version=self.recipe.catalogue_version,
|
||||
)
|
||||
|
||||
@property
|
||||
def shard_binding_digest(self) -> str:
|
||||
"""This Shard's own bytes and exact range, bound to the source artifact.
|
||||
|
||||
The fingerprint is range-independent on purpose — see the module
|
||||
docstring — which means it cannot distinguish two *different splits of
|
||||
the same source*. Both are the same recipe on the same model, and for
|
||||
route compatibility that is exactly right. It is not right for
|
||||
certification: a derivative could otherwise assert the correct source and
|
||||
inherit a certification earned by bytes it does not hold.
|
||||
|
||||
So the derivative's own content hash and exact end-exclusive range get a
|
||||
second digest, outside the fingerprint. The Tracker re-derives it at
|
||||
admission and requires certification evidence to name the same binding
|
||||
for every serving node.
|
||||
"""
|
||||
binding = self.artifact.derived_from
|
||||
return _digest(
|
||||
SHARD_BINDING_DIGEST_DOMAIN,
|
||||
{
|
||||
"source_digest": self.artifact.source_digest,
|
||||
"content_digest": self.artifact.content_digest,
|
||||
"architecture_digest": self.artifact.architecture_digest,
|
||||
"derivative_range": (
|
||||
None
|
||||
if binding is None
|
||||
else [binding.shard_start, binding.shard_end]
|
||||
),
|
||||
"shard_start": self.shard_start,
|
||||
"shard_end": self.shard_end,
|
||||
},
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"schema_version": RECIPE_IDENTITY_SCHEMA_VERSION,
|
||||
@@ -747,57 +770,6 @@ def explain_mismatch(
|
||||
return tuple(mismatches)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DistributedForwardEvidence:
|
||||
"""Proof that a recipe really ran, distributed, end to end.
|
||||
|
||||
This is the only thing that takes a recipe out of the dark. The bar is
|
||||
deliberately the product's bar, not a unit test's: at least two *distinct*
|
||||
physical nodes, whose Shards cover the whole model with no hole, generating
|
||||
real tokens. A synthetic worker is a unit fixture and certifies nothing —
|
||||
the whole risk this guards against is a recipe that passes in a mock and
|
||||
produces garbage on real weights.
|
||||
"""
|
||||
|
||||
route_session_id: str
|
||||
route_epoch: int
|
||||
node_ids: tuple[str, ...]
|
||||
shard_ranges: tuple[tuple[int, int], ...]
|
||||
tokens_generated: int
|
||||
layer_count: int
|
||||
synthetic: bool = False
|
||||
certified_at: float = field(default_factory=time.time)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_text(self.route_session_id, "evidence.route_session_id")
|
||||
_require_int(self.route_epoch, "evidence.route_epoch", 0)
|
||||
_require_int(self.tokens_generated, "evidence.tokens_generated", 0)
|
||||
_require_int(self.layer_count, "evidence.layer_count", 1)
|
||||
|
||||
def rejection(self) -> str | None:
|
||||
"""Why this evidence does not certify, or None when it does."""
|
||||
if self.synthetic:
|
||||
return (
|
||||
"evidence comes from a synthetic worker; only a real distributed "
|
||||
"forward certifies a recipe"
|
||||
)
|
||||
distinct = set(self.node_ids)
|
||||
if len(distinct) < MIN_CERTIFYING_NODES:
|
||||
return (
|
||||
f"evidence covers {len(distinct)} distinct node(s); a distributed "
|
||||
f"forward requires at least {MIN_CERTIFYING_NODES}"
|
||||
)
|
||||
if len(distinct) != len(self.node_ids):
|
||||
return "the same node is counted more than once in the certifying route"
|
||||
if self.tokens_generated < 1:
|
||||
return "the certifying forward generated no tokens"
|
||||
|
||||
gap = _coverage_gap(self.shard_ranges, self.layer_count)
|
||||
if gap is not None:
|
||||
return gap
|
||||
return None
|
||||
|
||||
|
||||
def _coverage_gap(
|
||||
ranges: Iterable[tuple[int, int]], layer_count: int
|
||||
) -> str | None:
|
||||
@@ -830,172 +802,20 @@ def _coverage_gap(
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RecipeStatus:
|
||||
"""What the registry knows about one fingerprint."""
|
||||
|
||||
status: str
|
||||
fingerprint: CompatibilityFingerprint | None = None
|
||||
detail: str = ""
|
||||
certified_at: float | None = None
|
||||
|
||||
@property
|
||||
def may_serve(self) -> bool:
|
||||
"""Only a certified recipe carries user traffic."""
|
||||
return self.status == STATUS_CERTIFIED
|
||||
|
||||
@property
|
||||
def may_certify(self) -> bool:
|
||||
"""A dark recipe is eligible for a certification route — and only that.
|
||||
|
||||
Without this, certification is unreachable: serving requires
|
||||
certification, certification requires a real distributed forward, and a
|
||||
real distributed forward requires a route. A dark recipe may therefore
|
||||
be routed *for the express purpose of certifying it*, and never for user
|
||||
traffic.
|
||||
"""
|
||||
return self.status in (STATUS_DARK, STATUS_CERTIFIED)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"status": self.status,
|
||||
"detail": self.detail,
|
||||
"certified_at": self.certified_at,
|
||||
"fingerprint": (
|
||||
self.fingerprint.to_dict() if self.fingerprint is not None else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class RecipeRegistry:
|
||||
"""Registered-but-dark recipes, and the ones a real forward has certified.
|
||||
|
||||
An unknown recipe is not routable. A known one is not routable either, until
|
||||
it has been proven. This is the fail-closed default the roadmap asks for:
|
||||
"unsupported architectures/backends remain registered-but-dark until real
|
||||
certification passes."
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._dark: dict[tuple[str, str], CompatibilityFingerprint] = {}
|
||||
self._certified: dict[tuple[str, str], RecipeStatus] = {}
|
||||
|
||||
def register(self, identity: ShardIdentity | CompatibilityFingerprint) -> RecipeStatus:
|
||||
"""Record a recipe as known. Known is not certified."""
|
||||
fingerprint = _as_fingerprint(identity)
|
||||
key = fingerprint.key
|
||||
if key in self._certified:
|
||||
return self._certified[key]
|
||||
self._dark[key] = fingerprint
|
||||
return RecipeStatus(
|
||||
STATUS_DARK,
|
||||
fingerprint,
|
||||
"registered; dark until a real distributed forward certifies it",
|
||||
)
|
||||
|
||||
def status(self, identity: ShardIdentity | CompatibilityFingerprint) -> RecipeStatus:
|
||||
fingerprint = _as_fingerprint(identity)
|
||||
key = fingerprint.key
|
||||
if key in self._certified:
|
||||
return self._certified[key]
|
||||
if key in self._dark:
|
||||
return RecipeStatus(
|
||||
STATUS_DARK,
|
||||
fingerprint,
|
||||
"registered; dark until a real distributed forward certifies it",
|
||||
)
|
||||
return RecipeStatus(
|
||||
STATUS_UNKNOWN,
|
||||
fingerprint,
|
||||
"this recipe has never been registered with the Tracker",
|
||||
)
|
||||
|
||||
def certify(
|
||||
self,
|
||||
identity: ShardIdentity | CompatibilityFingerprint,
|
||||
evidence: DistributedForwardEvidence,
|
||||
) -> RecipeStatus:
|
||||
"""Promote a dark recipe, if the evidence is a real distributed forward.
|
||||
|
||||
Raises rather than returning a failed status: certifying is a state
|
||||
change, and a caller that ignores a returned failure would leave a
|
||||
recipe it believes to be certified in the dark.
|
||||
"""
|
||||
fingerprint = _as_fingerprint(identity)
|
||||
rejection = evidence.rejection()
|
||||
if rejection is not None:
|
||||
raise RecipeNotCertified(
|
||||
f"this evidence does not certify {fingerprint.recipe_id!r}: {rejection}"
|
||||
)
|
||||
status = RecipeStatus(
|
||||
STATUS_CERTIFIED,
|
||||
fingerprint,
|
||||
(
|
||||
f"certified by route session {evidence.route_session_id} across "
|
||||
f"{len(set(evidence.node_ids))} nodes"
|
||||
),
|
||||
certified_at=evidence.certified_at,
|
||||
)
|
||||
self._certified[fingerprint.key] = status
|
||||
self._dark.pop(fingerprint.key, None)
|
||||
return status
|
||||
|
||||
def require_serving(
|
||||
self, identity: ShardIdentity | CompatibilityFingerprint
|
||||
) -> RecipeStatus:
|
||||
"""Admit a recipe for user traffic, or refuse and say why."""
|
||||
status = self.status(identity)
|
||||
if not status.may_serve:
|
||||
raise RecipeNotCertified(
|
||||
f"recipe {_as_fingerprint(identity).recipe_id!r} is {status.status}: "
|
||||
f"{status.detail}"
|
||||
)
|
||||
return status
|
||||
|
||||
def certified_fingerprints(self) -> tuple[CompatibilityFingerprint, ...]:
|
||||
return tuple(
|
||||
status.fingerprint
|
||||
for status in self._certified.values()
|
||||
if status.fingerprint is not None
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"schema_version": RECIPE_IDENTITY_SCHEMA_VERSION,
|
||||
"dark": [fp.to_dict() for fp in self._dark.values()],
|
||||
"certified": [status.to_dict() for status in self._certified.values()],
|
||||
}
|
||||
|
||||
|
||||
def _as_fingerprint(
|
||||
identity: ShardIdentity | CompatibilityFingerprint,
|
||||
) -> CompatibilityFingerprint:
|
||||
if isinstance(identity, CompatibilityFingerprint):
|
||||
return identity
|
||||
if isinstance(identity, ShardIdentity):
|
||||
return identity.fingerprint
|
||||
raise RecipeIdentityError(
|
||||
f"expected a ShardIdentity or CompatibilityFingerprint, got "
|
||||
f"{type(identity).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def check_route(
|
||||
shards: Sequence[ShardIdentity],
|
||||
*,
|
||||
registry: RecipeRegistry | None = None,
|
||||
for_certification: bool = False,
|
||||
) -> tuple[RouteMismatch, ...]:
|
||||
"""Decide whether these Shards may form one Inference Route.
|
||||
"""Why these Shards may not form one Inference Route; empty when they may.
|
||||
|
||||
Returns every reason they may not, empty when they may. Fail-closed by
|
||||
construction: an empty route, an unknown recipe, a hole in the layer
|
||||
coverage and a single differing dtype all produce a reason, and a caller that
|
||||
only ever proceeds on an empty tuple cannot accidentally admit any of them.
|
||||
This answers the *numerical* question only — do these Shards agree on every
|
||||
axis that moves the numbers, and do they tile the model without a hole. It
|
||||
deliberately does not answer "may this recipe carry user traffic": that is
|
||||
certification, the Tracker owns it (`meshnet_tracker.recipe`), and a node
|
||||
asking itself whether it is certified would be marking its own homework.
|
||||
|
||||
`for_certification` admits a dark recipe onto a route whose only purpose is
|
||||
to certify it. It never admits an unknown one, and it is not the path user
|
||||
traffic takes.
|
||||
Fail-closed by construction: an empty route, a hole in the coverage and a
|
||||
single differing dtype each produce a reason, so a caller that proceeds only
|
||||
on an empty tuple cannot admit any of them.
|
||||
"""
|
||||
if not shards:
|
||||
return (
|
||||
@@ -1018,27 +838,12 @@ def check_route(
|
||||
if gap is not None:
|
||||
mismatches.append(RouteMismatch(MISMATCH_RANGE, gap))
|
||||
|
||||
if registry is not None:
|
||||
status = registry.status(head.fingerprint)
|
||||
allowed = status.may_certify if for_certification else status.may_serve
|
||||
if not allowed:
|
||||
mismatches.append(
|
||||
RouteMismatch(MISMATCH_UNCERTIFIED, f"{status.status}: {status.detail}")
|
||||
)
|
||||
|
||||
return tuple(mismatches)
|
||||
|
||||
|
||||
def require_route(
|
||||
shards: Sequence[ShardIdentity],
|
||||
*,
|
||||
registry: RecipeRegistry | None = None,
|
||||
for_certification: bool = False,
|
||||
) -> CompatibilityFingerprint:
|
||||
def require_route(shards: Sequence[ShardIdentity]) -> CompatibilityFingerprint:
|
||||
"""`check_route`, raising the structured reasons instead of returning them."""
|
||||
mismatches = check_route(
|
||||
shards, registry=registry, for_certification=for_certification
|
||||
)
|
||||
mismatches = check_route(shards)
|
||||
if mismatches:
|
||||
raise RouteIncompatible(mismatches)
|
||||
return shards[0].fingerprint
|
||||
@@ -1134,14 +939,111 @@ def check_handshake(
|
||||
return tuple(reasons)
|
||||
|
||||
|
||||
def check_session_open(
|
||||
local: ShardIdentity,
|
||||
remote: "pb.SessionOpen",
|
||||
*,
|
||||
expected_route_session_id: str | None = None,
|
||||
expected_route_epoch: int | None = None,
|
||||
) -> tuple[RouteMismatch, ...]:
|
||||
"""Fail closed on the complete immutable portion of a ``SessionOpen``.
|
||||
|
||||
``SessionOpen`` is not just a fingerprint carrier: its route session and
|
||||
epoch select the tracker-issued route whose identity was checked. A worker
|
||||
that accepts a stale epoch can receive activations for a route that the
|
||||
tracker has already replaced. Callers that have the tracker assignment
|
||||
therefore pass both expected values; callers without it still reject the
|
||||
protobuf defaults, which cannot identify a live route.
|
||||
"""
|
||||
reasons = list(check_handshake(local, remote.fingerprint))
|
||||
if remote.schema_version != SCHEMA_VERSION:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_BOUNDARY_SCHEMA,
|
||||
f"peer schema version {remote.schema_version} does not match "
|
||||
f"{SCHEMA_VERSION}",
|
||||
)
|
||||
)
|
||||
|
||||
if not remote.route_session_id:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_ROUTE_SESSION,
|
||||
"peer omitted the tracker-issued route session id",
|
||||
)
|
||||
)
|
||||
elif (
|
||||
expected_route_session_id is not None
|
||||
and remote.route_session_id != expected_route_session_id
|
||||
):
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_ROUTE_SESSION,
|
||||
"peer route session does not match the tracker assignment",
|
||||
)
|
||||
)
|
||||
|
||||
if remote.route_epoch < 1:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_ROUTE_EPOCH,
|
||||
"peer omitted a positive tracker-issued route epoch",
|
||||
)
|
||||
)
|
||||
elif expected_route_epoch is not None and remote.route_epoch != expected_route_epoch:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_ROUTE_EPOCH,
|
||||
"peer route epoch does not match the tracker assignment",
|
||||
)
|
||||
)
|
||||
|
||||
advertised = (remote.shard_range.start_layer, remote.shard_range.end_layer)
|
||||
expected = (local.shard_start, local.shard_end)
|
||||
effective = remote.shard_range.effective_start_layer
|
||||
if advertised != expected:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_RANGE,
|
||||
f"peer advertised Shard range {advertised}, expected {expected}",
|
||||
)
|
||||
)
|
||||
elif not local.shard_start <= effective < local.shard_end:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_RANGE,
|
||||
f"effective start {effective} is outside Shard range {expected}",
|
||||
)
|
||||
)
|
||||
return tuple(reasons)
|
||||
|
||||
|
||||
def handshake_error(
|
||||
mismatches: Sequence[RouteMismatch],
|
||||
) -> "pb.ShardError | None":
|
||||
"""The protocol status a rejected handshake closes the stream with."""
|
||||
"""The protocol status a rejected handshake closes the stream with.
|
||||
|
||||
Each rejection maps to the most specific DGR-002 code the reasons allow:
|
||||
a digest disagreement dominates (the peer is the wrong artifact or recipe,
|
||||
whatever else is also wrong), then a schema disagreement (a peer on a
|
||||
foreign schema cannot be trusted to have encoded anything else correctly),
|
||||
and only a pure range disagreement reports the range code. Collapsing all
|
||||
of these to one code would leave the initiator unable to tell "re-route me"
|
||||
from "renegotiate the schema" from "wrong peer entirely".
|
||||
"""
|
||||
if not mismatches:
|
||||
return None
|
||||
reasons = {mismatch.reason for mismatch in mismatches}
|
||||
if reasons == {MISMATCH_RANGE}:
|
||||
code = pb.ERROR_CODE_SHARD_RANGE_MISMATCH
|
||||
elif reasons <= {MISMATCH_ROUTE_SESSION, MISMATCH_ROUTE_EPOCH}:
|
||||
code = pb.ERROR_CODE_EPOCH_STALE
|
||||
elif reasons <= {MISMATCH_BOUNDARY_SCHEMA, MISMATCH_RANGE}:
|
||||
code = pb.ERROR_CODE_SCHEMA_UNSUPPORTED
|
||||
else:
|
||||
code = pb.ERROR_CODE_FINGERPRINT_MISMATCH
|
||||
return pb.ShardError(
|
||||
code=pb.ERROR_CODE_FINGERPRINT_MISMATCH,
|
||||
code=code,
|
||||
detail="; ".join(m.describe() for m in mismatches),
|
||||
retryable=False,
|
||||
)
|
||||
|
||||
@@ -185,6 +185,10 @@ class CapabilityState:
|
||||
# Absent for a node that predates DGR-003.
|
||||
model_artifact_digest: str | None = None
|
||||
runtime_recipe_digest: str | None = None
|
||||
shard_binding_digest: str | None = None
|
||||
# The tracker ledger's verdict on that fingerprint at evaluation time
|
||||
# ("dark"/"certified"), so the network map answers "why is this exact node
|
||||
# not routing" without a second query. None when no identity was presented.
|
||||
certification: str | None = None
|
||||
|
||||
@property
|
||||
@@ -227,6 +231,7 @@ class CapabilityState:
|
||||
"diagnostics": list(self.diagnostics),
|
||||
"model_artifact_digest": self.model_artifact_digest,
|
||||
"runtime_recipe_digest": self.runtime_recipe_digest,
|
||||
"shard_binding_digest": self.shard_binding_digest,
|
||||
"certification": self.certification,
|
||||
}
|
||||
|
||||
@@ -372,6 +377,22 @@ def evaluate_report(
|
||||
f"identity is for artifact {identity.artifact_id!r}, but the node "
|
||||
f"registered {advertised_model!r}",
|
||||
)
|
||||
model_claim = report["model"]
|
||||
if model_claim.get("revision") != identity.revision:
|
||||
return base.with_state(
|
||||
STATE_MODEL_MISMATCH,
|
||||
"identity revision does not match the capability proof",
|
||||
)
|
||||
config_fingerprint = model_claim.get("config_fingerprint")
|
||||
if isinstance(config_fingerprint, str) and config_fingerprint.startswith(
|
||||
"sha256:"
|
||||
):
|
||||
config_fingerprint = config_fingerprint.removeprefix("sha256:")
|
||||
if config_fingerprint != identity.architecture_digest:
|
||||
return base.with_state(
|
||||
STATE_MODEL_MISMATCH,
|
||||
"identity architecture/config digest does not match the capability proof",
|
||||
)
|
||||
if (
|
||||
identity.recipe_id != base.recipe_id
|
||||
or identity.recipe_version != base.recipe_version
|
||||
@@ -399,6 +420,7 @@ def evaluate_report(
|
||||
base,
|
||||
model_artifact_digest=identity.model_artifact_digest,
|
||||
runtime_recipe_digest=identity.runtime_recipe_digest,
|
||||
shard_binding_digest=identity.shard_binding_digest,
|
||||
)
|
||||
|
||||
if status != STATUS_PASSED:
|
||||
@@ -425,8 +447,22 @@ def evaluate_report(
|
||||
# not authenticate a node or prove a distributed forward. Exact recipes are
|
||||
# therefore registered-but-dark until tracker-owned certification records
|
||||
# that forward.
|
||||
#
|
||||
# `ledger is None` means the caller owns no certification authority. It is
|
||||
# not "certify anything" and it is not "make one up": a disposable ledger
|
||||
# would register the recipe into state that is discarded on return, which
|
||||
# reads like certification is wired when nothing is recording it. The only
|
||||
# safe reading is that nothing here has certified this recipe, so it stays
|
||||
# dark. `TrackerServer` owns the real ledger and passes it in.
|
||||
if identity is not None:
|
||||
recipe_status = (ledger or CertificationLedger()).register(identity)
|
||||
if ledger is None:
|
||||
return base.with_state(
|
||||
STATE_UNCERTIFIED,
|
||||
"no certification ledger; an exact recipe is dark until a "
|
||||
"tracker-owned distributed forward certifies it",
|
||||
)
|
||||
recipe_status = ledger.register(identity)
|
||||
base = replace(base, certification=recipe_status.status)
|
||||
if not recipe_status.may_serve:
|
||||
return base.with_state(STATE_UNCERTIFIED, recipe_status.detail)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import json
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable, Mapping, Sequence
|
||||
from typing import Any, Iterable, Mapping
|
||||
|
||||
# Layout of the identity block this tracker reads (meshnet_node.runtime_recipe).
|
||||
RECIPE_IDENTITY_SCHEMA_VERSION = 1
|
||||
@@ -42,6 +42,7 @@ RECIPE_IDENTITY_SCHEMA_VERSION = 1
|
||||
# without changing it there silently partitions every route.
|
||||
ARTIFACT_DIGEST_DOMAIN = "meshnet.model-artifact.v1"
|
||||
RECIPE_DIGEST_DOMAIN = "meshnet.runtime-recipe.v1"
|
||||
SHARD_BINDING_DIGEST_DOMAIN = "meshnet.shard-binding.v1"
|
||||
|
||||
# The axes a recipe digest commits to. Order is irrelevant (the canonical JSON
|
||||
# sorts keys); membership is not — an axis missing here is an axis the tracker
|
||||
@@ -62,16 +63,6 @@ RECIPE_AXES: tuple[str, ...] = (
|
||||
|
||||
_INT_AXES = frozenset({"boundary_schema_version", "protocol_schema_version"})
|
||||
|
||||
MISMATCH_ARTIFACT = "artifact"
|
||||
MISMATCH_TOKENIZER = "tokenizer"
|
||||
MISMATCH_ARCHITECTURE = "architecture"
|
||||
MISMATCH_RANGE = "range"
|
||||
MISMATCH_BOUNDARY_SCHEMA = "boundary-schema"
|
||||
MISMATCH_ACTIVATION_RECIPE = "activation-recipe"
|
||||
MISMATCH_CACHE_LAYOUT = "cache-layout"
|
||||
MISMATCH_FINGERPRINT = "fingerprint"
|
||||
MISMATCH_UNCERTIFIED = "uncertified"
|
||||
|
||||
STATUS_UNKNOWN = "unknown"
|
||||
STATUS_DARK = "dark"
|
||||
STATUS_CERTIFIED = "certified"
|
||||
@@ -183,6 +174,8 @@ class PresentedIdentity:
|
||||
artifact_id: str
|
||||
revision: str
|
||||
source_digest: str
|
||||
content_digest: str
|
||||
derivative_range: tuple[int, int] | None
|
||||
architecture: str
|
||||
architecture_digest: str
|
||||
layer_count: int
|
||||
@@ -211,6 +204,36 @@ class PresentedIdentity:
|
||||
def key(self) -> tuple[str, str]:
|
||||
return (self.model_artifact_digest, self.runtime_recipe_digest)
|
||||
|
||||
@property
|
||||
def shard_binding_digest(self) -> str:
|
||||
"""This participant's own bytes and exact range, bound to the source.
|
||||
|
||||
The route fingerprint (:attr:`key`) is deliberately range-independent —
|
||||
Shards on one route own different ranges, so a range-sensitive digest
|
||||
would stop any two of them from ever agreeing. The consequence is that
|
||||
the fingerprint alone cannot tell two *different splits of the same
|
||||
source* apart: a derivative can assert the right source and recipe and
|
||||
inherit certification earned by bytes it does not hold.
|
||||
|
||||
This digest is what closes that. It commits to the derivative's own
|
||||
content hash and its exact end-exclusive range, so certification can be
|
||||
recipe-wide while every serving node is still separately pinned to the
|
||||
blob it was admitted on (`TrackerServer.certify_recipe`).
|
||||
"""
|
||||
return _digest(
|
||||
SHARD_BINDING_DIGEST_DOMAIN,
|
||||
{
|
||||
"source_digest": self.source_digest,
|
||||
"content_digest": self.content_digest,
|
||||
"architecture_digest": self.architecture_digest,
|
||||
"derivative_range": list(self.derivative_range)
|
||||
if self.derivative_range is not None
|
||||
else None,
|
||||
"shard_start": self.shard_start,
|
||||
"shard_end": self.shard_end,
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def tokenizer_revision(self) -> str:
|
||||
return str(self.axes["tokenizer_revision"])
|
||||
@@ -315,6 +338,8 @@ def parse_identity(data: Any) -> PresentedIdentity:
|
||||
artifact_id=_text(artifact.get("artifact_id"), "artifact.artifact_id"),
|
||||
revision=_pin(artifact.get("revision"), "artifact.revision"),
|
||||
source_digest=source_digest,
|
||||
content_digest=content_digest,
|
||||
derivative_range=binding,
|
||||
architecture=_text(artifact.get("architecture"), "artifact.architecture"),
|
||||
architecture_digest=_hex64(
|
||||
artifact.get("architecture_digest"), "artifact.architecture_digest"
|
||||
@@ -349,78 +374,6 @@ def parse_identity(data: Any) -> PresentedIdentity:
|
||||
return identity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RouteMismatch:
|
||||
reason: str
|
||||
detail: str
|
||||
|
||||
def describe(self) -> str:
|
||||
return f"{self.reason}: {self.detail}"
|
||||
|
||||
|
||||
def compare(
|
||||
route: PresentedIdentity, candidate: PresentedIdentity
|
||||
) -> tuple[RouteMismatch, ...]:
|
||||
"""Why `candidate` may not join a route already running `route`."""
|
||||
if route.key == candidate.key:
|
||||
return ()
|
||||
|
||||
reasons: list[RouteMismatch] = []
|
||||
if route.source_digest != candidate.source_digest:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_ARTIFACT,
|
||||
f"serves Model Artifact {candidate.source_digest[:12]}…, the route "
|
||||
f"runs {route.source_digest[:12]}…",
|
||||
)
|
||||
)
|
||||
if route.architecture_digest != candidate.architecture_digest:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_ARCHITECTURE,
|
||||
"architecture/config snapshot differs from the route's",
|
||||
)
|
||||
)
|
||||
if route.layer_count != candidate.layer_count:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_ARTIFACT,
|
||||
f"artifact has {candidate.layer_count} layers, the route's has "
|
||||
f"{route.layer_count}",
|
||||
)
|
||||
)
|
||||
|
||||
axis_reason = {
|
||||
"tokenizer_revision": MISMATCH_TOKENIZER,
|
||||
"architecture_adapter": MISMATCH_ARCHITECTURE,
|
||||
"activation_dtype": MISMATCH_ACTIVATION_RECIPE,
|
||||
"compute_dtype": MISMATCH_ACTIVATION_RECIPE,
|
||||
"kv_dtype": MISMATCH_CACHE_LAYOUT,
|
||||
"kv_layout": MISMATCH_CACHE_LAYOUT,
|
||||
"boundary_schema_version": MISMATCH_BOUNDARY_SCHEMA,
|
||||
"protocol_schema_version": MISMATCH_BOUNDARY_SCHEMA,
|
||||
}
|
||||
for axis in RECIPE_AXES:
|
||||
mine = route.axes.get(axis)
|
||||
theirs = candidate.axes.get(axis)
|
||||
if mine != theirs:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
axis_reason.get(axis, MISMATCH_FINGERPRINT),
|
||||
f"{axis} is {theirs!r}, the route runs {mine!r}",
|
||||
)
|
||||
)
|
||||
|
||||
if not reasons:
|
||||
reasons.append(
|
||||
RouteMismatch(
|
||||
MISMATCH_FINGERPRINT,
|
||||
"fingerprints differ on a field this comparison does not cover",
|
||||
)
|
||||
)
|
||||
return tuple(reasons)
|
||||
|
||||
|
||||
def coverage_gap(
|
||||
ranges: Iterable[tuple[int, int]], layer_count: int
|
||||
) -> str | None:
|
||||
@@ -462,6 +415,7 @@ class DistributedForwardEvidence:
|
||||
tokens_generated: int
|
||||
layer_count: int
|
||||
fingerprint: tuple[str, str]
|
||||
participants: tuple[PresentedIdentity, ...]
|
||||
synthetic: bool = False
|
||||
certified_at: float = field(default_factory=time.time)
|
||||
|
||||
@@ -471,6 +425,19 @@ class DistributedForwardEvidence:
|
||||
"evidence comes from a synthetic worker; only a real distributed "
|
||||
"forward certifies a recipe"
|
||||
)
|
||||
if len(self.participants) != len(self.node_ids):
|
||||
return "participant identities do not match the certifying node list"
|
||||
if len(self.shard_ranges) != len(self.participants):
|
||||
return "participant identities do not match the recorded effective ranges"
|
||||
for participant, shard_range in zip(
|
||||
self.participants, self.shard_ranges, strict=True
|
||||
):
|
||||
if participant.key != self.fingerprint:
|
||||
return "a participant fingerprint differs from the certifying route"
|
||||
if participant.layer_count != self.layer_count:
|
||||
return "a participant artifact layer count differs from the certifying route"
|
||||
if (participant.shard_start, participant.shard_end) != shard_range:
|
||||
return "a participant identity does not match its recorded effective range"
|
||||
distinct = set(self.node_ids)
|
||||
if len(distinct) < MIN_CERTIFYING_NODES:
|
||||
return (
|
||||
@@ -540,11 +507,11 @@ class CertificationLedger:
|
||||
|
||||
def certify(
|
||||
self,
|
||||
identity: PresentedIdentity | tuple[str, str],
|
||||
identity: PresentedIdentity,
|
||||
evidence: DistributedForwardEvidence,
|
||||
) -> RecipeStatus:
|
||||
"""Promote a recipe out of the dark, or raise saying why the evidence is short."""
|
||||
key = identity if isinstance(identity, tuple) else identity.key
|
||||
key = identity.key
|
||||
if key not in self._dark and key not in self._certified:
|
||||
raise RecipeIdentityError(
|
||||
"this fingerprint is not registered; unknown recipes cannot be certified"
|
||||
@@ -553,6 +520,10 @@ class CertificationLedger:
|
||||
raise RecipeIdentityError(
|
||||
"certification evidence fingerprint does not match the recipe being promoted"
|
||||
)
|
||||
if evidence.layer_count != identity.layer_count:
|
||||
raise RecipeIdentityError(
|
||||
"certification evidence layer count does not match the artifact being promoted"
|
||||
)
|
||||
rejection = evidence.rejection()
|
||||
if rejection is not None:
|
||||
raise RecipeIdentityError(f"this evidence does not certify: {rejection}")
|
||||
@@ -578,38 +549,8 @@ class CertificationLedger:
|
||||
}
|
||||
|
||||
|
||||
def admit_route(
|
||||
identities: Sequence[PresentedIdentity],
|
||||
*,
|
||||
ledger: CertificationLedger | None = None,
|
||||
for_certification: bool = False,
|
||||
) -> tuple[RouteMismatch, ...]:
|
||||
"""Every reason these Shards may not form one Inference Route; empty when they may.
|
||||
|
||||
Fail-closed by construction: an empty route, a hole in the coverage, one
|
||||
differing dtype, and an uncertified recipe each produce a reason, so a caller
|
||||
that proceeds only on an empty result cannot admit any of them.
|
||||
"""
|
||||
if not identities:
|
||||
return (RouteMismatch(MISMATCH_RANGE, "a route needs at least one Shard"),)
|
||||
|
||||
head = identities[0]
|
||||
reasons: list[RouteMismatch] = []
|
||||
for candidate in identities[1:]:
|
||||
reasons.extend(compare(head, candidate))
|
||||
|
||||
gap = coverage_gap(
|
||||
[(i.shard_start, i.shard_end) for i in identities], head.layer_count
|
||||
)
|
||||
if gap is not None:
|
||||
reasons.append(RouteMismatch(MISMATCH_RANGE, gap))
|
||||
|
||||
if ledger is not None:
|
||||
status = ledger.status(head)
|
||||
allowed = status.may_certify if for_certification else status.may_serve
|
||||
if not allowed:
|
||||
reasons.append(
|
||||
RouteMismatch(MISMATCH_UNCERTIFIED, f"{status.status}: {status.detail}")
|
||||
)
|
||||
|
||||
return tuple(reasons)
|
||||
# Route formation itself lives in `server.py` (`_select_route`, `_enumerate_routes`,
|
||||
# `_find_pinned_route`): candidates are partitioned by the exact fingerprint this
|
||||
# module derives, so a route can only ever be assembled inside one identity. A
|
||||
# second route gate here would be a second copy of that policy to keep in step —
|
||||
# the same reason the node module holds no certification authority.
|
||||
|
||||
@@ -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