Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions compute_horde/compute_horde/manifest_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
Utilities for working with miner manifests and knowledge commitments.

These functions are shared between miner and validator for consistent
manifest format and parsing.
"""

import logging

from compute_horde_core.executor_class import ExecutorClass

logger = logging.getLogger(__name__)

MAX_COMMITMENT_LENGTH = 128


def format_manifest_commitment(manifest: dict[ExecutorClass, int]) -> str:
"""
Converts manifest dict to commitment string format.

Example: "spin_up-4min.gpu-24gb=2;always_on.gpu-24gb=3"

- Sorted by executor class name for consistency
- No trailing semicolon
- Max 128 chars

Raises:
ValueError: If the resulting string exceeds MAX_COMMITMENT_LENGTH
"""
if not manifest:
return ""

# Sort by executor class value (string) for consistency
sorted_items = sorted(manifest.items(), key=lambda x: x[0].value)

# Format as key=value pairs joined by semicolon
parts = [f"{executor_class.value}={count}" for executor_class, count in sorted_items]
commitment_string = ";".join(parts)

if len(commitment_string) > MAX_COMMITMENT_LENGTH:
raise ValueError(
f"Commitment string length {len(commitment_string)} exceeds maximum {MAX_COMMITMENT_LENGTH}"
)

return commitment_string


def parse_commitment_string(commitment: str) -> dict[ExecutorClass, int]:
"""
Parses commitment string back to manifest dict.

Returns empty dict if invalid/empty.
"""
if not commitment or not commitment.strip():
return {}

manifest = {}
try:
# Split by semicolon
pairs = commitment.split(";")
for pair in pairs:
if "=" not in pair:
logger.warning(f"Invalid commitment pair (missing '='): {pair}")
continue

key, value = pair.split("=", 1)
key = key.strip()
value = value.strip()

# Try to parse as ExecutorClass
try:
executor_class = ExecutorClass(key)
count = int(value)
manifest[executor_class] = count
except (ValueError, KeyError) as e:
logger.warning(f"Invalid commitment pair {pair}: {e}")
continue

except Exception as e:
logger.error(f"Error parsing commitment string '{commitment}': {e}")
return {}

return manifest
77 changes: 77 additions & 0 deletions miner/app/src/compute_horde_miner/miner/manifest_commitment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import logging

import bittensor
from compute_horde.manifest_utils import (
format_manifest_commitment,
parse_commitment_string,
)
from compute_horde_core.executor_class import ExecutorClass

logger = logging.getLogger(__name__)

# Re-export for backward compatibility
__all__ = [
"format_manifest_commitment",
"parse_commitment_string",
"has_manifest_changed",
"commit_manifest_to_subtensor",
]


def has_manifest_changed(
current_manifest: dict[ExecutorClass, int], chain_commitment: str | None
) -> bool:
"""
Compare current manifest with on-chain commitment.

Returns True if different or if no commitment exists.
"""
if not chain_commitment:
return True

chain_manifest = parse_commitment_string(chain_commitment)
return current_manifest != chain_manifest


def commit_manifest_to_subtensor(
manifest: dict[ExecutorClass, int],
wallet: bittensor.wallet,
subtensor: bittensor.subtensor,
netuid: int,
) -> bool:
"""
Commits manifest to knowledge commitments.

Returns True if successful, False otherwise.

Steps:
1. Format manifest to commitment string
2. Validate length <= 128 chars
3. Call subtensor.commit(wallet, netuid, commitment_string)
4. Handle rate limiting (100 blocks)

Note: Empty manifests are allowed to support "pausing" a miner.
"""
try:
# Format manifest to commitment string (empty manifests result in empty string)
commitment_string = format_manifest_commitment(manifest)

logger.info(f"Committing manifest to chain: {commitment_string or '(empty)'}")

# Commit to subtensor (empty string is valid for pausing)
# Note: subtensor.commit() is rate limited to 100 blocks
success = subtensor.commit(wallet, netuid, commitment_string)

if success:
logger.info("Successfully committed manifest to chain")
return True
else:
logger.warning("Failed to commit manifest to chain")
return False

except ValueError as e:
logger.error(f"Invalid manifest format: {e}")
return False
except Exception as e:
logger.error(f"Error committing manifest to subtensor: {e}", exc_info=True)
return False
61 changes: 61 additions & 0 deletions miner/app/src/compute_horde_miner/miner/tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime

import bittensor
from asgiref.sync import async_to_sync
from celery.utils.log import get_task_logger
from compute_horde.dynamic_config import fetch_dynamic_configs_from_contract, sync_dynamic_config
Expand All @@ -11,6 +12,11 @@

from compute_horde_miner.celery import app
from compute_horde_miner.miner import eviction, quasi_axon
from compute_horde_miner.miner.executor_manager import current
from compute_horde_miner.miner.manifest_commitment import (
commit_manifest_to_subtensor,
has_manifest_changed,
)
from compute_horde_miner.miner.models import Validator
from compute_horde_miner.miner.receipts import current_store

Expand Down Expand Up @@ -96,3 +102,58 @@ def archive_receipt_pages():
return

store.archive_old_pages()


@app.task
def commit_manifest_to_chain():
"""
Periodically checks and commits manifest if changed.

Steps:
1. Get current manifest from executor_manager
2. Get on-chain commitment via subtensor.get_commitment()
3. Compare: has_manifest_changed()
4. If changed:
a. Commit to subtensor
b. Log success/failure

Note: Empty manifests are allowed to support "pausing" a miner.

Error handling:
- Rate limit: Skip silently, will retry next cycle
- Connection errors: Log warning, retry next cycle
- Format errors: Log critical error
"""
try:
# Get current manifest (empty manifest is valid for pausing)
manifest = async_to_sync(current.executor_manager.get_manifest)()

# Get on-chain commitment
wallet = settings.BITTENSOR_WALLET()
subtensor = bittensor.subtensor(network=settings.BITTENSOR_NETWORK)

try:
# Get commitment for this miner's hotkey
chain_commitment = subtensor.get_commitment(
netuid=settings.BITTENSOR_NETUID,
hotkey=wallet.hotkey.ss58_address,
)
except Exception as e:
logger.warning(f"Failed to get on-chain commitment: {e}")
chain_commitment = None

# Check if manifest has changed
if has_manifest_changed(manifest, chain_commitment):
logger.info("Manifest has changed, committing to chain")
success = commit_manifest_to_subtensor(
manifest, wallet, subtensor, settings.BITTENSOR_NETUID
)
if success:
logger.info("Successfully committed manifest to chain")
else:
logger.warning("Failed to commit manifest to chain")
else:
logger.debug("Manifest unchanged, skipping commitment")

except Exception as e:
logger.error(f"Error in commit_manifest_to_chain: {e}", exc_info=True)
Loading