diff --git a/publisher_verify.py b/publisher_verify.py index d19e5c49c..017466ff1 100644 --- a/publisher_verify.py +++ b/publisher_verify.py @@ -856,6 +856,31 @@ def _reactivate(conn): pm_closed.status_code == 503 and pm_closed.json().get("detail") == "per_miner_seed_secret_missing") os.environ["CATHEDRAL_PERMINER_SEED_SECRET"] = "publisher-verify-stable-seed" + _pm._instance_index.cache_clear() + os.environ["CATHEDRAL_PERMINER_ALLOTMENT_T1"] = "3" + cache_epoch = _pm.current_epoch() + cache_id = _pm.instance_id("cache-hotkey", cache_epoch, 1, 2) + cache_first = _pm.recover_tier_seq_for("cache-hotkey", cache_epoch, cache_id) + cache_info_first = _pm._instance_index.cache_info() + cache_second = _pm.recover_tier_seq_for("cache-hotkey", cache_epoch, cache_id) + cache_info_second = _pm._instance_index.cache_info() + cache_foreign = _pm.recover_tier_seq_for("cache-other-hotkey", cache_epoch, cache_id) + os.environ["CATHEDRAL_PERMINER_ALLOTMENT_T1"] = "2" + cache_reduced_allotment = _pm.recover_tier_seq_for("cache-hotkey", cache_epoch, cache_id) + os.environ["CATHEDRAL_PERMINER_ALLOTMENT_T1"] = "3" + os.environ["CATHEDRAL_PERMINER_SEED_SECRET"] = "publisher-verify-cache-rotated" + cache_rotated = _pm.recover_tier_seq_for("cache-hotkey", cache_epoch, cache_id) + ck("per-miner recovery index is cached and seed-scoped", + cache_first == (1, 2) + and cache_second == (1, 2) + and cache_info_first.misses >= 1 + and cache_info_second.hits > cache_info_first.hits + and cache_foreign is None + and cache_reduced_allotment is None + and cache_rotated is None) + os.environ["CATHEDRAL_PERMINER_SEED_SECRET"] = "publisher-verify-stable-seed" + os.environ["CATHEDRAL_PERMINER_ALLOTMENT_T1"] = "1" + _pm._instance_index.cache_clear() def _map_pm_coldkey(conn): conn.execute( "INSERT OR REPLACE INTO coldkey_map(hotkey, coldkey, updated_at_iso) " diff --git a/scaffold/publisher/per_miner.py b/scaffold/publisher/per_miner.py index d1da66082..f1f0e9718 100644 --- a/scaffold/publisher/per_miner.py +++ b/scaffold/publisher/per_miner.py @@ -125,6 +125,12 @@ def allotment_for(tier: int) -> int: {1: 10_000, 2: 10_000}.get(tier, 10_000))) +# Worst-case entries are roughly cache_size * allotment_for(tier). +_RECOVER_INDEX_CACHE_SIZE = max( + 1, _env_int("CATHEDRAL_PERMINER_RECOVER_INDEX_CACHE", 64) +) + + def assignment_page_limit_max() -> int: return max(1, min(500, _env_int("CATHEDRAL_PERMINER_MAX_PAGE_LIMIT", 50))) @@ -237,6 +243,10 @@ def _seed_secret_bytes() -> bytes: return _EPHEMERAL_SEED_SECRET +def _seed_secret_fingerprint() -> str: + return hashlib.sha256(_seed_secret_bytes()).hexdigest() + + # -------------------------------------------------------------------------- # Per-miner instance set generation # -------------------------------------------------------------------------- @@ -369,19 +379,33 @@ def verify_miner_submission_for( def recover_tier_seq_for(hotkey: str, epoch: int, challenge_id: str) -> tuple[int, int] | None: - """Find (tier, seq) for a challenge_id by scanning the miner's allotment. + """Find (tier, seq) for a challenge_id in the miner's allotment. Returns None if the challenge_id was not generated for this hotkey+epoch. """ parsed = parse_challenge_id(challenge_id) - candidate_tiers = [parsed["tier"]] if parsed and parsed["tier"] in TIERS else TIERS - for tier in candidate_tiers: - for seq in range(allotment_for(tier)): - cid = instance_id(hotkey, epoch, tier, seq) - if cid == challenge_id: - return tier, seq + if not parsed or parsed["epoch"] != int(epoch) or parsed["tier"] not in TIERS: + return None + tier = parsed["tier"] + seq = _instance_index(_recover_index_key(hotkey, epoch, tier)).get(challenge_id) + if seq is not None: + return tier, seq return None +def _recover_index_key(hotkey: str, epoch: int, tier: int) -> tuple[str, int, int, int, str]: + return hotkey, int(epoch), int(tier), allotment_for(tier), _seed_secret_fingerprint() + + +@lru_cache(maxsize=_RECOVER_INDEX_CACHE_SIZE) +def _instance_index(key: tuple[str, int, int, int, str]) -> dict[str, int]: + """Shared read-only cid->seq cache; do not mutate the returned dict.""" + hotkey, epoch, tier, allotment, _secret_fingerprint = key + return { + instance_id(hotkey, epoch, tier, seq): seq + for seq in range(allotment) + } + + def recover_seq_for(hotkey: str, epoch: int, challenge_id: str) -> int | None: """Find the seq number for a challenge_id. Kept for backward compatibility.""" result = recover_tier_seq_for(hotkey, epoch, challenge_id)