diff --git a/CHANGELOG.md b/CHANGELOG.md index 197fe38..dc66530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 3718d87..5c9155a 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -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" diff --git a/docker-compose.prd.yaml b/docker-compose.prd.yaml index f9cb377..c5d0b20 100644 --- a/docker-compose.prd.yaml +++ b/docker-compose.prd.yaml @@ -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: diff --git a/docker-compose.stg.yaml b/docker-compose.stg.yaml index ba98688..695dadb 100644 --- a/docker-compose.stg.yaml +++ b/docker-compose.stg.yaml @@ -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" diff --git a/docker-compose.validator.yaml b/docker-compose.validator.yaml index a6fc2aa..4bb3f52 100644 --- a/docker-compose.validator.yaml +++ b/docker-compose.validator.yaml @@ -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" diff --git a/neurons/validator/main.py b/neurons/validator/main.py index 920ef8b..9cbb33e 100644 --- a/neurons/validator/main.py +++ b/neurons/validator/main.py @@ -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) @@ -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( diff --git a/neurons/validator/tasks/run_agents.py b/neurons/validator/tasks/run_agents.py index 5dc2d1e..6732729 100644 --- a/neurons/validator/tasks/run_agents.py +++ b/neurons/validator/tasks/run_agents.py @@ -1,6 +1,7 @@ import asyncio import json import uuid +from datetime import datetime from pathlib import Path from typing import List, Optional @@ -32,6 +33,7 @@ class RunAgents(AbstractTask): logger: NuminousLogger max_concurrent_sandboxes: int timeout_seconds: int + sync_hour: int def __init__( self, @@ -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).") @@ -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", @@ -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) diff --git a/neurons/validator/tasks/tests/test_run_agents.py b/neurons/validator/tasks/tests/test_run_agents.py index e748ac8..f90f5a0 100644 --- a/neurons/validator/tasks/tests/test_run_agents.py +++ b/neurons/validator/tasks/tests/test_run_agents.py @@ -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() diff --git a/neurons/validator/tests/test_main.py b/neurons/validator/tests/test_main.py index ca89bcf..3324220 100644 --- a/neurons/validator/tests/test_main.py +++ b/neurons/validator/tests/test_main.py @@ -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 diff --git a/neurons/validator/utils/config.py b/neurons/validator/utils/config.py index ac6ba9a..f53b865 100644 --- a/neurons/validator/utils/config.py +++ b/neurons/validator/utils/config.py @@ -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) @@ -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()) @@ -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 diff --git a/neurons/validator/utils/tests/test_config.py b/neurons/validator/utils/tests/test_config.py index 780baa2..2e277c2 100644 --- a/neurons/validator/utils/tests/test_config.py +++ b/neurons/validator/utils/tests/test_config.py @@ -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() @@ -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 @@ -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() @@ -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""" @@ -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") @@ -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 @@ -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() diff --git a/neurons/validator/version.py b/neurons/validator/version.py index a97d2cf..5489182 100644 --- a/neurons/validator/version.py +++ b/neurons/validator/version.py @@ -1,4 +1,4 @@ -__version__ = "2.0.0" +__version__ = "2.0.1" version_split = __version__.split(".")