Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
39c9852
Add Spec Version for Weight Set
bkb2135 Aug 9, 2025
d3e8736
WIP
dbobrenko Aug 9, 2025
cc04840
Update unit tests
bkb2135 Aug 9, 2025
7c153f1
Add logs, reduce frequency
dbobrenko Aug 9, 2025
4b35624
Add verbose logs
dbobrenko Aug 9, 2025
915671f
Add logs file and fix tests
dbobrenko Aug 9, 2025
7c0219a
Bump patch version
dbobrenko Aug 9, 2025
dad2cb8
Add more verbose logs
dbobrenko Aug 9, 2025
7355c96
Reduce loops to 5
dbobrenko Aug 9, 2025
7c22d89
Adjust logging
dbobrenko Aug 9, 2025
06c43fc
Adjust timeouts
dbobrenko Aug 9, 2025
cebee21
Fix sampling
dbobrenko Aug 9, 2025
7847159
Add error handling
dbobrenko Aug 9, 2025
d8e96d6
Reduce logs persistency
dbobrenko Aug 9, 2025
86e61c3
Fix set weights result parsing
dbobrenko Aug 9, 2025
53a5f29
Revert sample size to 50
dbobrenko Aug 9, 2025
4583804
Fix mypy
dbobrenko Aug 9, 2025
8fdecf6
Fix mypy
dbobrenko Aug 9, 2025
710e4d1
Fix tests
dbobrenko Aug 9, 2025
51c4ece
Merge with spec branch
dbobrenko Aug 9, 2025
de71240
Add Autoupdater
bkb2135 Aug 10, 2025
9bbdc3f
Refactor autoupdate module: reorder imports and improve docstring for…
bkb2135 Aug 10, 2025
ce34019
Enhance docstrings and improve autoupdate termination handling in aut…
bkb2135 Aug 10, 2025
a354d70
Add weight synchronizer
dbobrenko Aug 10, 2025
59057b1
Fix weight syncer
dbobrenko Aug 10, 2025
d65aeed
Clean up the code
dbobrenko Aug 10, 2025
150628c
Set main config by default
dbobrenko Aug 10, 2025
423ff01
Add weight synchronizer (#796)
dbobrenko Aug 10, 2025
c47bf62
Merge branch 'release/v3.0.1' into features/autoupdate
dbobrenko Aug 10, 2025
e0fe131
Merge branch 'release/v3.0.1' into features/autoupdate
dbobrenko Aug 10, 2025
32b1d63
Add shutdown for weight syncer
dbobrenko Aug 10, 2025
491c11d
Add tests to autoupdater
dbobrenko Aug 10, 2025
606b912
Clean up packages
dbobrenko Aug 10, 2025
dddeada
Testing autoupdater
dbobrenko Aug 10, 2025
8aff434
Testing autoupdater 2
dbobrenko Aug 10, 2025
b38f591
Cleaning up files
dbobrenko Aug 10, 2025
10072e4
Add pm2 script
dbobrenko Aug 10, 2025
014064b
Fix pm2
dbobrenko Aug 10, 2025
893d2ff
Fix tests
dbobrenko Aug 10, 2025
9b8c110
Add deps
dbobrenko Aug 10, 2025
5be01c1
Update README
dbobrenko Aug 10, 2025
21fcc3a
Update pm2
dbobrenko Aug 10, 2025
7cf1218
Remove test values
dbobrenko Aug 10, 2025
640eaf0
Remove test values in pm2 script
dbobrenko Aug 10, 2025
f73ae7a
Fix logging
dbobrenko Aug 10, 2025
8fcbadf
Lower task generation frequency
dbobrenko Aug 10, 2025
e3ce414
Fix readme
dbobrenko Aug 10, 2025
3123c98
Fix formatting
dbobrenko Aug 10, 2025
2fc458c
Merge pull request #795 from macrocosm-os/features/autoupdate
dbobrenko Aug 10, 2025
6802c3b
Ensure port is of type int
dbobrenko Aug 11, 2025
91f9801
Add constants timeout variable
dbobrenko Aug 11, 2025
676b187
Run formatter
dbobrenko Aug 11, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.log
requirements.txt
**/*.ipynb
debug_rewards.jsonl
Expand Down
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,22 @@ repos:
hooks:
- id: ruff
args: [--fix]
stages: [pre-commit]
- id: ruff-format
stages: [pre-commit]

- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy .
language: system
pass_filenames: false
stages: [pre-commit]

- id: pytest
name: pytest
entry: pytest tests/ --verbose --failed-first --exitfirst --disable-warnings
language: system
pass_filenames: false
stages: [pre-commit]
33 changes: 13 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,34 @@ Subnet 1 is the most intelligent inference model on Bittensor. As the first agen

---

## Installation
## Run Validator

1. **Clone the repository:**
```bash
git clone https://github.com/macrocosm-os/apex.git
cd apex
```

2. **Install `uv`:**
Follow the instructions at [https://github.com/astral-sh/uv](https://github.com/astral-sh/uv) to install `uv`. For example:

2. **Prepare config file:**
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
cp config/mainnet.yaml.example config/mainnet.yaml
# Fill in the required values in config/mainnet.yaml
```

3. **Install the project and its development dependencies:**
3. **[Recommended] Run validator with auto-updater:**
```bash
uv venv && uv python install 3.11 && uv python pin 3.11 && uv venv --python=3.11 && uv pip install -e '.[dev]'
python scripts/autoupdater.py -c config/mainnet.yaml
```

4. **Activate python environment:**
```bash
. .venv/bin/activate
```

## Run Mainnet Validator

1. Prepare config file:
4. **[Alternative #1] Run validator with pm2 and auto-updater:**
```bash
cp config/mainnet.yaml.example config/mainnet.yaml
# Fill in the required values in config/mainnet.yaml
bash scripts/autoupdater_pm2.sh
```

2. **Run the validator:**
5. **[Alternative #2] Install dependencies and run validator without auto-updater:**
```bash
python validator.py -c config/mainnet.yaml
uv venv --python 3.11 && uv pip install '.[dev]' && python validator.py -c config/mainnet.yaml
```

## Run Testnet Validator
Expand All @@ -69,9 +62,9 @@ Subnet 1 is the most intelligent inference model on Bittensor. As the first agen
# Fill in the required values in config/testnet.yaml
```

2. **Run the validator:**
2. Install dependencies and run validator:
```bash
python validator.py -c config/testnet.yaml
uv venv --python 3.11 && uv pip install '.[dev]' && python validator.py -c config/testnet.yaml
```

## Base Miner (for showcase purposes only)
Expand Down
15 changes: 13 additions & 2 deletions apex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
__version__ = version("apex")


def _version_to_int(version_str: str) -> int:
version_split = version_str.split(".") + ["0", "0"] # in case a version doesn't have third element, e.g. 3.0
major = int(version_split[0])
minor = int(version_split[1])
patch = int(version_split[2])
return (10000 * major) + (100 * minor) + patch


__spec_version__ = _version_to_int(__version__)


def setup_logger(log_file_path: str | Path | None = None, level: str = "INFO") -> Any:
"""Set up the loguru logger with optional file logging and specified log level.

Expand All @@ -28,9 +39,9 @@ def setup_logger(log_file_path: str | Path | None = None, level: str = "INFO") -
# Add file handler if a path is provided.
if log_file_path:
file_log_format = "{time:YYYY-MM-DD HH:mm:ss} [{file}:{line}] {message}"
logger.add(str(log_file_path), level=level, format=file_log_format, rotation="10 MB", retention="7 days")
logger.add(str(log_file_path), level=level, format=file_log_format, rotation="5 MB", retention="3 days")

return logger


setup_logger(level="DEBUG")
setup_logger(log_file_path="logs.log", level="DEBUG")
39 changes: 19 additions & 20 deletions apex/common/async_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from bittensor.core.metagraph import AsyncMetagraph
from loguru import logger

from apex import __spec_version__
from apex.common.utils import async_cache

_METAGRAPH_TTL: int = 10 * 60
Expand All @@ -15,7 +16,6 @@ def __init__(self, coldkey: str, hotkey: str, netuid: int, network: list[str] |
if isinstance(network, str):
network = [network]
self._network: list[str] = network

self._coldkey = coldkey
self._hotkey = hotkey
self._netuid = netuid
Expand Down Expand Up @@ -111,36 +111,35 @@ def network(self) -> list[str]:
return self._network

async def set_weights(self, rewards: dict[str, float]) -> bool:
metagraph = await self.metagraph()
subtensor = await self.subtensor()
weights: dict[int, float] = {}
try:
metagraph = await self.metagraph()
subtensor = await self.subtensor()
weights: dict[int, float] = {}

for hotkey, reward in rewards.items():
try:
idx = metagraph.hotkeys.index(hotkey)
except ValueError:
# Hotkey not found in the metagraph (e.g., deregistered). Skip it.
continue
for hotkey, reward in rewards.items():
try:
idx = metagraph.hotkeys.index(hotkey)
except ValueError:
# Hotkey not found in the metagraph (e.g., deregistered). Skip it.
continue

uid = metagraph.uids[idx]
weights[uid] = reward
uid = metagraph.uids[idx]
weights[uid] = reward

# Set the weights.
try:
result = await subtensor.set_weights(
success, err = await subtensor.set_weights(
wallet=self._wallet,
netuid=self._netuid,
uids=list(weights.keys()),
weights=list(weights.values()),
version_key=__spec_version__,
wait_for_inclusion=True,
wait_for_finalization=True,
)
if not result:
logger.error(f"Error setting weights: {result}")
return False
return True
if not success:
logger.error(f"Error during weight set: {err}")
return bool(success)
except BaseException as exc:
logger.exception(f"Error setting weights: {exc}")
logger.exception(f"Error during weight set: {exc}")
return False

async def mask_network(self) -> list[str]:
Expand Down
1 change: 1 addition & 0 deletions apex/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Config(BaseModel):
chain: ConfigClass = Field(default_factory=ConfigClass)
websearch: ConfigClass = Field(default_factory=ConfigClass)
logger_db: ConfigClass = Field(default_factory=ConfigClass)
weight_syncer: ConfigClass = Field(default_factory=ConfigClass)
miner_sampler: ConfigClass = Field(default_factory=ConfigClass)
miner_scorer: ConfigClass = Field(default_factory=ConfigClass)
llm: ConfigClass = Field(default_factory=ConfigClass)
Expand Down
9 changes: 9 additions & 0 deletions apex/common/constants.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
TIMEOUT: float = 20
MAX_TOKENS: int = 2048
TEMPERATURE: float = 0.1
WEBPAGE_MAXSIZE: int = 500
VALIDATOR_REFERENCE_LABEL = "Validator"
VALIDATOR_VERIFIED_HOTKEYS = {
"5CGLCBndTR1BvQZzn429ckT8GyxduzyjMgt4K1UVTYa8gKfb": "167.99.236.79:8001", # Macrocosmos.
"5CUbyC2Ez7tWYYmnFSSwjqkw26dFNo9cXH8YmcxBSfxi2XSG": None, # Yuma.
"5C8Em1kDZi5rxgDN4zZtfoT7dUqJ7FFbTzS3yTP5GPgVUsn1": None, # RoundTable21.
"5HmkM6X1D3W3CuCSPuHhrbYyZNBy2aGAiZy9NczoJmtY25H7": None, # Crucible.
"5GeR3cDuuFKJ7p66wKGjY65MWjWnYqffq571ZMV4gKMnJqK5": None, # OTF.
"5D1saVvssckE1XoPwPzdHrqYZtvBJ3vESsrPNxZ4zAxbKGs1": None, # Rizzo.
}


_ENGLISH_WORDS: tuple[str, ...] | None = None
Expand Down
87 changes: 86 additions & 1 deletion apex/common/epistula.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import json
import time
from hashlib import sha256
from math import ceil
from typing import Any
from typing import Annotated, Any
from uuid import uuid4

from fastapi import HTTPException, Request
from loguru import logger
from substrateinterface import Keypair

from apex.common.async_chain import AsyncChain
from apex.common.constants import VALIDATOR_VERIFIED_HOTKEYS


async def generate_header(
hotkey: Keypair,
Expand Down Expand Up @@ -33,3 +39,82 @@ async def generate_header(
"0x" + hotkey.sign(str(timestamp_interval + 1) + "." + signed_for).hex()
)
return headers


def verify_signature(
signature: str | None,
body: bytes,
timestamp: str | None,
uuid: str | None,
signed_for: str,
signed_by: str,
now: float,
) -> Annotated[str, "Error Message"] | None:
if not isinstance(signature, str):
return "Invalid Signature"
if not isinstance(timestamp, str) or not timestamp.isdigit():
return "Invalid Timestamp"
timestamp_as_int = int(timestamp)
if not isinstance(signed_by, str):
return "Invalid Sender key"
if not isinstance(signed_for, str):
return "Invalid receiver key"
if not isinstance(uuid, str):
return "Invalid uuid"
if not isinstance(body, bytes):
return "Body is not of type bytes"
allowed_delta_ms = 8000
keypair = Keypair(ss58_address=signed_by)
if timestamp_as_int + allowed_delta_ms < now:
return "Request is too stale"
message = f"{sha256(body).hexdigest()}.{uuid}.{timestamp}.{signed_for}"
verified = keypair.verify(message, signature)
if not verified:
return "Signature Mismatch"
return None


async def verify_validator_signature(request: Request, chain: AsyncChain, min_stake: float = 1024) -> None:
signed_by = request.headers.get("Epistula-Signed-By")
signed_for = request.headers.get("Epistula-Signed-For")
if not signed_by or not signed_for:
logger.error("Missing Epistula-Signed-* headers")
raise HTTPException(400, "Missing Epistula-Signed-* headers")

wallet = chain.wallet
if signed_for != wallet.hotkey.ss58_address:
logger.error("Bad Request, message is not intended for self")
raise HTTPException(status_code=400, detail="Bad Request, message is not intended for self")

is_validator = True
if min_stake > 0:
metagraph = await chain.metagraph()
try:
caller_uid = metagraph.hotkeys.index(signed_by)
except ValueError as exc:
raise HTTPException(status_code=401, detail="Signer is not in metagraph") from exc
is_validator = metagraph.stake[caller_uid] > min_stake

if signed_by not in VALIDATOR_VERIFIED_HOTKEYS and not is_validator:
logger.error(f"Signer not the expected ss58 address: {signed_by}")
raise HTTPException(status_code=401, detail="Signer not the expected ss58 address")

now = time.time()
body: bytes = await request.body()
try:
json.loads(body)
except json.JSONDecodeError as exc:
raise HTTPException(400, "Invalid JSON body") from exc

err = verify_signature(
request.headers.get("Epistula-Request-Signature"),
body,
request.headers.get("Epistula-Timestamp"),
request.headers.get("Epistula-Uuid"),
signed_for,
signed_by,
now,
)
if err:
logger.error(err)
raise HTTPException(status_code=400, detail=err)
Loading