Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Release Notes

## [2.0.1] - 2025-11-28
- **Validator**: Added validator synchronization hour configuration

## [2.0.0] - 2025-11-25
- **Architecture**: Validator architecture re-implemented as code-submission system. Validators execute miner Python agents in Docker sandboxes. Miners no longer run nodes.
- **Scoring**: Replaced peer scoring with Brier scoring and winner-take-all weight distribution.
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ services:
- INLINE_LOGS=1

command: >
bash -c "python neurons/validator.py --netuid 155 --subtensor.network test --wallet.name testkey --wallet.hotkey hkey4 --db.directory /root/infinite_games/database --backend.gateway_url https://stg.numinous.earth --logging.debug"
bash -c "python neurons/validator.py --netuid 155 --subtensor.network test --wallet.name testkey --wallet.hotkey hkey4 --db.directory /root/infinite_games/database --backend.gateway_url https://stg.numinous.earth --validator.sync_hour 0 --logging.debug"

logging:
driver: "json-file"
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.prd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ services:
--wallet.hotkey ifhkey
--db.directory /root/infinite_games/database
--numinous.env prod
--sandbox.max_concurrent 50
--sandbox.max_concurrent 25
--sandbox.timeout_seconds 150
--validator.sync_hour 0
--logging.debug"

logging:
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.stg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ services:
- INLINE_LOGS=1

command: >
bash -c "python neurons/validator.py --netuid 155 --subtensor.network test --wallet.name testkey --wallet.hotkey hkey2 --db.directory /root/infinite_games/database --backend.gateway_url https://stg.numinous.earth --logging.debug"
bash -c "python neurons/validator.py --netuid 155 --subtensor.network test --wallet.name testkey --wallet.hotkey hkey2 --db.directory /root/infinite_games/database --backend.gateway_url https://stg.numinous.earth --validator.sync_hour 0 --logging.debug"

logging:
driver: "json-file"
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.validator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ services:
--wallet.hotkey ${WALLET_HOTKEY}
--db.directory /root/infinite_games/database
--numinous.env prod
--sandbox.max_concurrent 50
--sandbox.max_concurrent 25
--sandbox.timeout_seconds 150
--logging.info"
--logging.debug"

logging:
driver: "json-file"
Expand Down
3 changes: 2 additions & 1 deletion neurons/validator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def main():
# Start session id
logger.start_session()

config, numinous_env, db_path, logging_level, gateway_url = get_config()
config, numinous_env, db_path, logging_level, gateway_url, validator_sync_hour = get_config()

# Loggers
override_loggers_level(logging_level)
Expand Down Expand Up @@ -129,6 +129,7 @@ async def main():
logger=logger,
max_concurrent_sandboxes=config.get("sandbox", {}).get("max_concurrent", 50),
timeout_seconds=config.get("sandbox", {}).get("timeout_seconds", 180),
sync_hour=validator_sync_hour,
)

export_predictions_task = ExportPredictions(
Expand Down
13 changes: 13 additions & 0 deletions neurons/validator/tasks/run_agents.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import json
import uuid
from datetime import datetime
from pathlib import Path
from typing import List, Optional

Expand Down Expand Up @@ -32,6 +33,7 @@ class RunAgents(AbstractTask):
logger: NuminousLogger
max_concurrent_sandboxes: int
timeout_seconds: int
sync_hour: int

def __init__(
self,
Expand All @@ -43,6 +45,7 @@ def __init__(
logger: NuminousLogger,
max_concurrent_sandboxes: int = 5,
timeout_seconds: int = 600,
sync_hour: int = 4,
):
if not isinstance(interval_seconds, float) or interval_seconds <= 0:
raise ValueError("interval_seconds must be a positive number (float).")
Expand Down Expand Up @@ -76,6 +79,7 @@ def __init__(
self.logger = logger
self.max_concurrent_sandboxes = max_concurrent_sandboxes
self.timeout_seconds = timeout_seconds
self.sync_hour = sync_hour

self.logger.info(
"RunAgents task initialized",
Expand All @@ -94,6 +98,15 @@ def interval_seconds(self) -> float:
return self.interval

async def run(self) -> None:
current_hour_utc = datetime.utcnow().hour

if current_hour_utc < self.sync_hour:
self.logger.debug(
"Before execution window",
extra={"current_hour": current_hour_utc, "sync_hour": self.sync_hour},
)
return

await self.metagraph.sync()

block = torch_or_numpy_to_int(self.metagraph.block)
Expand Down
83 changes: 83 additions & 0 deletions neurons/validator/tasks/tests/test_run_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,3 +1166,86 @@ async def test_logs_exported_on_result_none(
logs = body.log_content

assert "Sandbox timeout - no logs" in logs

async def test_run_skips_when_before_sync_hour(
self, mock_db_operations, mock_sandbox_manager, mock_metagraph, mock_api_client, mock_logger
):
from unittest.mock import patch

task = RunAgents(
interval_seconds=600.0,
db_operations=mock_db_operations,
sandbox_manager=mock_sandbox_manager,
metagraph=mock_metagraph,
api_client=mock_api_client,
logger=mock_logger,
sync_hour=10,
)

with patch("neurons.validator.tasks.run_agents.datetime") as mock_datetime:
mock_now = MagicMock()
mock_now.hour = 5
mock_datetime.utcnow.return_value = mock_now

await task.run()

mock_logger.debug.assert_called_with(
"Before execution window",
extra={"current_hour": 5, "sync_hour": 10},
)
mock_metagraph.sync.assert_not_called()
mock_db_operations.get_events_to_predict.assert_not_called()

async def test_run_executes_when_at_sync_hour(
self, mock_db_operations, mock_sandbox_manager, mock_metagraph, mock_api_client, mock_logger
):
from unittest.mock import patch

mock_db_operations.get_events_to_predict.return_value = []

task = RunAgents(
interval_seconds=600.0,
db_operations=mock_db_operations,
sandbox_manager=mock_sandbox_manager,
metagraph=mock_metagraph,
api_client=mock_api_client,
logger=mock_logger,
sync_hour=10,
)

with patch("neurons.validator.tasks.run_agents.datetime") as mock_datetime:
mock_now = MagicMock()
mock_now.hour = 10
mock_datetime.utcnow.return_value = mock_now

await task.run()

mock_metagraph.sync.assert_called_once()
mock_db_operations.get_events_to_predict.assert_called_once()

async def test_run_executes_when_after_sync_hour(
self, mock_db_operations, mock_sandbox_manager, mock_metagraph, mock_api_client, mock_logger
):
from unittest.mock import patch

mock_db_operations.get_events_to_predict.return_value = []

task = RunAgents(
interval_seconds=600.0,
db_operations=mock_db_operations,
sandbox_manager=mock_sandbox_manager,
metagraph=mock_metagraph,
api_client=mock_api_client,
logger=mock_logger,
sync_hour=10,
)

with patch("neurons.validator.tasks.run_agents.datetime") as mock_datetime:
mock_now = MagicMock()
mock_now.hour = 15
mock_datetime.utcnow.return_value = mock_now

await task.run()

mock_metagraph.sync.assert_called_once()
mock_db_operations.get_events_to_predict.assert_called_once()
10 changes: 9 additions & 1 deletion neurons/validator/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@ def test_main(
# Mock get_config
logger_level = 99
gateway_url = "https://test.numinous.earth"
get_config.return_value = MagicMock(), config_env, db_path, logger_level, gateway_url
validator_sync_hour = 4
get_config.return_value = (
MagicMock(),
config_env,
db_path,
logger_level,
gateway_url,
validator_sync_hour,
)

# Mock IfMetagraph
mock_if_metagraph = MockIfMetagraph.return_value
Expand Down
9 changes: 8 additions & 1 deletion neurons/validator/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ def get_config():
default=180,
help="Timeout for agent execution in seconds (default: 180)",
)
parser.add_argument(
"--validator.sync_hour",
type=int,
default=4,
help="Hour for validator synchronization (default: 4)",
)

AsyncSubtensor.add_args(parser=parser)
LoggingMachine.add_args(parser=parser)
Expand All @@ -68,6 +74,7 @@ def get_config():
logging_trace = args.__getattribute__("logging.trace")
logging_debug = args.__getattribute__("logging.debug")
logging_info = args.__getattribute__("logging.info")
validator_sync_hour = args.__getattribute__("validator.sync_hour")

# Set default, __getattribute__ doesn't return arguments defaults
db_directory = args.__getattribute__("db.directory") or str(Path.cwd())
Expand Down Expand Up @@ -110,4 +117,4 @@ def get_config():
elif logging_info:
logging_level = logging.INFO

return config, env, db_path, logging_level, gateway_url
return config, env, db_path, logging_level, gateway_url, validator_sync_hour
11 changes: 7 additions & 4 deletions neurons/validator/utils/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def create_mock_args(
logging_trace=None,
logging_debug=None,
logging_info=None,
validator_sync_hour=4,
):
"""Helper method to create a mock args object with the required attributes"""
mock_args = MagicMock()
Expand All @@ -59,6 +60,7 @@ def create_mock_args(
"logging.trace": logging_trace,
"logging.debug": logging_debug,
"logging.info": logging_info,
"validator.sync_hour": validator_sync_hour,
}.get(x)

return mock_args
Expand All @@ -68,7 +70,7 @@ def test_arg_additions(self, mock_subtensor, mock_logging_machine, mock_wallet):
with patch("argparse.ArgumentParser.parse_args") as mock_parse_args:
mock_parse_args.return_value = self.create_mock_args(netuid=6, network="finney")

config, env, db_path, logger_level, gateway_url = get_config()
config, env, db_path, logger_level, gateway_url, validator_sync_hour = get_config()

mock_subtensor.add_args.assert_called_once()
mock_logging_machine.add_args.assert_called_once()
Expand All @@ -78,6 +80,7 @@ def test_arg_additions(self, mock_subtensor, mock_logging_machine, mock_wallet):
assert env == "prod"
assert db_path == str(Path(DEFAULT_DB_DIRECTORY) / "validator.db")
assert logger_level == logging.WARNING
assert validator_sync_hour == 4

def test_required_args_missing(self):
"""Test behavior when required arguments are missing"""
Expand All @@ -96,7 +99,7 @@ def test_db_directory_validation(self):
mock_parse_args.return_value = self.create_mock_args(
netuid=6, network="finney", numinous_env=None, db_directory=valid_dir
)
_, env, db_path, _, _ = get_config()
_, env, db_path, _, _, _ = get_config()

assert env == "prod"
assert db_path == str(Path(valid_dir) / "validator.db")
Expand Down Expand Up @@ -144,7 +147,7 @@ def test_logger_args(self, logging_trace, logging_debug, logging_info, expected_
logging_info=logging_info,
)

_, _, _, logger_level, _ = get_config()
_, _, _, logger_level, _, _ = get_config()

assert logger_level == expected_logger_level

Expand Down Expand Up @@ -210,7 +213,7 @@ def test_configurations(
)

if expected_valid:
_, env, db_path, _, _ = get_config()
_, env, db_path, _, _, _ = get_config()

mock_config.assert_called_once()

Expand Down
2 changes: 1 addition & 1 deletion neurons/validator/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.0.0"
__version__ = "2.0.1"

version_split = __version__.split(".")

Expand Down
Loading