import logging import pathlib from datetime import datetime from enum import Enum from typing import TYPE_CHECKING, Literal import bittensor from datura.chain import ( DEFAULT_ENDPOINT_RETRY_AFTER_SECONDS, PUBLIC_NODE_SOURCE, ChainEndpoint, chain_endpoint_candidates, ) from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict if TYPE_CHECKING: from bittensor import Wallet from incentive.config import IncentiveConfig from lium_core.shared_config import DEFAULT_SHARED_CONFIG, SharedConfigClient logger = logging.getLogger(__name__) class FeatureFlag(str, Enum): """Feature flag names for type-safe access.""" VERIFYX_NETWORK_VALIDATION = "verifyx_network_validation" class VerifyXSettings(BaseSettings): """VerifyX configuration - configurable thresholds for validation. Set via environment variables prefixed with VERIFYX_ (e.g., VERIFYX_MEMORY_MIN_TEST_GB=16). Use .env for local development (git-ignored). """ model_config = SettingsConfigDict(env_prefix="VERIFYX_", env_file=".env", extra="ignore", hide_input_in_errors=True) # Memory configuration MEMORY_ALLOCATION_PERCENTAGE: int = Field( default=75, description="Percentage of RAM to allocate for testing" ) MEMORY_MIN_TEST_GB: int = Field( default=8, description="Minimum RAM required in GB" ) MEMORY_MAX_TEST_GB: int = Field( default=128, description="Maximum RAM to test in GB" ) # Storage configuration STORAGE_MIN_AVAILABLE_GB: int = Field( default=100, description="Minimum storage space required in GB" ) STORAGE_THROUGHPUT_TEST_GB: int = Field( default=5, description="Size of data to test storage throughput in GB" ) # Network configuration NETWORK_TIMEOUT_SECONDS: int = Field( default=120, description="Timeout for network tests in seconds" ) NETWORK_MIN_DOWNLOAD_SPEED_MBPS: float = Field( default=50.0, description="Minimum Cloudflare capacity download speed in Mbps", ) NETWORK_MIN_PACKAGE_DOWNLOAD_SPEED_MBPS: float = Field( default=50.0, description="Minimum package (integrity object) download speed in Mbps; also the Cloudflare fallback floor", ) # When LIBRARY_REFRESH_ENABLED is on and the executor's libverifyx.so hash does not # match, curl this URL once, install, check the hash, and retry. The validator's own # file is the source of truth if the fetch hash differs. Off by default: a mismatch # does not write /usr/lib on the provider host. LIBRARY_REFRESH_ENABLED: bool = Field( default=False, description="If true, a libverifyx.so hash mismatch may replace /usr/lib/libverifyx.so on the executor", ) LIBRARY_FETCH_URL: str = Field( default="https://raw.githubusercontent.com/Datura-ai/lium-io/main/neurons/executor/libverifyx.so", description="Raw GitHub URL the executor curls when library refresh is on and the hash does not match", ) class DebugSettings(BaseSettings): """Debug configuration - all flags default to False/None. Set via environment variables prefixed with DEBUG_ (e.g., DEBUG_SKIP_STAKE_CHECKS=true). Use .env for local development (git-ignored). """ model_config = SettingsConfigDict(env_prefix="DEBUG_", env_file=".env", extra="ignore", hide_input_in_errors=True) ENABLED: bool = Field(default=False, description="Enable debug mode") USE_LOCAL_MINER: bool = Field(default=False, description="Use local miner") MINER_HOTKEY: str | None = Field(default=None, description="Miner hotkey") MINER_COLDKEY: str | None = Field(default=None, description="Miner coldkey") MINER_UID: int | None = Field(default=None, description="Miner UID") MINER_ADDRESS: str | None = Field(default=None, description="Miner address") MINER_PORT: int | None = Field(default=None, description="Miner port") SKIP_STORAGE_CHECK: bool = Field(default=False, description="Skip storage check") class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore", hide_input_in_errors=True) PROJECT_NAME: str = "compute-subnet-validator" BITTENSOR_WALLET_DIRECTORY: pathlib.Path = Field( env="BITTENSOR_WALLET_DIRECTORY", default=pathlib.Path("~").expanduser() / ".bittensor" / "wallets", ) BITTENSOR_WALLET_NAME: str = Field(env="BITTENSOR_WALLET_NAME") BITTENSOR_WALLET_HOTKEY_NAME: str = Field(env="BITTENSOR_WALLET_HOTKEY_NAME") BITTENSOR_NETUID: int = Field(env="BITTENSOR_NETUID", default=51) BITTENSOR_CHAIN_ENDPOINT: str | None = Field(env="BITTENSOR_CHAIN_ENDPOINT", default=None) # Ordered, comma-separated: our proxy first, the public node is always appended last. BITTENSOR_CHAIN_ENDPOINTS: str | None = Field(env="BITTENSOR_CHAIN_ENDPOINTS", default=None) # A failed endpoint is not dialled again for this long; the next ones in the list serve meanwhile. BITTENSOR_CHAIN_ENDPOINT_RETRY_AFTER_SECONDS: int = Field( env="BITTENSOR_CHAIN_ENDPOINT_RETRY_AFTER_SECONDS", default=DEFAULT_ENDPOINT_RETRY_AFTER_SECONDS ) BITTENSOR_NETWORK: str = Field(env="BITTENSOR_NETWORK", default="finney") SQLALCHEMY_DATABASE_URI: str = Field(env="SQLALCHEMY_DATABASE_URI") ASYNC_SQLALCHEMY_DATABASE_URI: str = Field(env="ASYNC_SQLALCHEMY_DATABASE_URI") INTERNAL_PORT: int = Field(env="INTERNAL_PORT", default=8000) BLOCKS_FOR_JOB: int = 75 # 15 minutes JOB_TIME_OUT: int = 60 * 15 # 15 minutes REDIS_HOST: str = Field(env="REDIS_HOST", default="localhost") REDIS_PORT: int = Field(env="REDIS_PORT", default=6379) REDIS_USERNAME: str | None = Field(env="REDIS_USERNAME", default=None) REDIS_PASSWORD: str | None = Field(env="REDIS_PASSWORD", default=None) BACKEND_COMMAND_CLAIM_TTL_SECONDS: int = Field(default=60, gt=0) BACKEND_COMMAND_HEARTBEAT_SECONDS: float = Field(default=20, gt=0) BACKEND_COMMAND_ANSWER_TTL_SECONDS: int = Field(default=900, gt=0) COMPUTE_APP_URI: str = Field(env="COMPUTE_APP_URI", default="wss://lium.io/api") COMPUTE_REST_API_URL: str | None = Field( env="COMPUTE_REST_API_URL", default="https://lium.io/api" ) COMPUTE_REST_API_URL_EXTERNAL: str | None = Field( env="COMPUTE_REST_API_URL_EXTERNAL", default="https://lium.io/api" ) MINER_PORTAL_REST_API_URL: str = Field( env="MINER_PORTAL_REST_API_URL", default="https://provider-api.lium.io/" ) TAO_PRICE_API_URL: str = Field(env="TAO_PRICE_API_URL", default="https://api.coingecko.com/api/v3/coins/bittensor") ENV: str = Field(env="ENV", default="dev") PORTION_FOR_UPTIME: float = 1 UPTIME_REQUIRED_MINUTES: int = 60 * 24 * 14 # 14 days PORTION_FOR_SYSBOX: float = 0.2 PORTION_FOR_SYSBOX_UNRENTED: float = 1 PORTION_FOR_SYSBOX_RENTED: float = 1 SYSBOX_RENTED_CUTOFF: datetime = datetime(2026, 4, 3, 12, 0, 0) # DAH-3597: first-miss grace for a DinD probe that never reached its container; the rule is # in PortConnectivityCheck, the TTL bounds the window between two misses. DIND_PROBE_FIRST_MISS_GRACE: bool = Field(env="DIND_PROBE_FIRST_MISS_GRACE", default=False) DIND_PROBE_FIRST_MISS_GRACE_TTL_SECONDS: int = Field( env="DIND_PROBE_FIRST_MISS_GRACE_TTL_SECONDS", default=3600, gt=0 ) # ticket-0494: how long a DinD probe's sysbox pass is remembered for a rented executor whose later # probes cannot reach the host (PortConnectivityCheck). Refreshed by every passing probe. SYSBOX_VERIFIED_TTL_SECONDS: int = Field(env="SYSBOX_VERIFIED_TTL_SECONDS", default=24 * 3600, gt=0) DISCORD_INCENTIVE_CUTOFF: datetime = datetime(2026, 6, 15, 12, 0, 0) # DAH-2265: cached-template requirement. Before the cutoff the CachedTemplateVerificationCheck # is purely advisory (publishes recommended_image_cached / recommended_image_digest_match to # executor.specs, never changes score). On/after the cutoff that check turns critical: an # unrented executor that has not pre-pulled the recommended default image, or holds stale # content under the same tag, fails verification early (score 0, reason surfaced to the # provider). Fails open: an unmeasured signal (None) is never penalised. # The cutoff has elapsed and CachedTemplateVerificationCheck is fatal again, so the gate is # live: unrented executors are enforced, rented ones are exempt (the check runs after the # tenant short-circuit). CACHED_TEMPLATE_CUTOFF: datetime = datetime(2026, 7, 14, 12, 0, 0) # A node this validator has just found without its recommended image is held as pending, not # failed, until the executor's first pre-pull sweep completes or GRACE_SECONDS after that first # sighting, whichever comes first. The express lane verifies a new node seconds after it is # added, long before a multi-GB pull can finish. GRACE_SECONDS=0 turns the grace off. CACHED_TEMPLATE_FRESH_NODE_GRACE_SECONDS: int = Field( env="CACHED_TEMPLATE_FRESH_NODE_GRACE_SECONDS", default=30 * 60, ge=0 ) # Minimum NVIDIA driver requirement. Compared as a dotted version tuple against the # executor's reported gpu.driver (e.g. "580.95.05"). 580.65.06 is the r580 floor that # ships CUDA 13.0. Before MIN_DRIVER_CUTOFF providers get a grace period (no penalty); # on/after the cutoff, non-compliant unrented executors are fully gated (multiplier 0). # Currently-rented executors are always exempt. MIN_NVIDIA_DRIVER_VERSION: str = Field(env="MIN_NVIDIA_DRIVER_VERSION", default="580.65.06") MIN_DRIVER_CUTOFF: datetime = datetime(2026, 6, 30, 12, 0, 0) TIME_DELTA_FOR_EMISSION: float = 0.01 # Read version from version.txt VERSION: str = (pathlib.Path(__file__).parent / ".." / ".." / "version.txt").read_text().strip() # Pre-DAH-2274 burner uids. They receive no burn (burn goes to NEW_BURNERS); they are only kept # in the miner list even when not serving (subtensor_client._build_serving_miners_with_opted_in_routing). BURNERS: list[int] = [4, 206, 207, 208] # All burn emission slots route to UID 47 (Datura payout key). NEW_BURNERS: list[int] = [47, 47, 47, 47, 47, 47, 47, 47, 47, 47] # Expected coldkeys for burner UIDs. Burn weights are withheld when coldkey mismatches. BURNER_COLDKEYS: dict[int, str] = { 47: "5G694c15wAu1LKi9rpSQqJjpBfg4K1oiBxEm5QSVdVZAfp9f", } # Comma-separated miner hotkeys that earn no subnet incentive from this validator. Their # executors are still validated, listed and rentable exactly as before; only the incentive # is zeroed (every pool, spot pay and referral), and the share they would have taken is # left to burn. Empty (the default) changes nothing. Burner hotkeys are never affected. SCORING_EXCLUDED_HOTKEYS: str = Field(env="SCORING_EXCLUDED_HOTKEYS", default="") # Comma-separated miner hotkeys whose unrented (idle) executors earn no incentive, same format # and effect as SCORING_EXCLUDED_HOTKEYS but only while not rented; rented GPUs score as usual. IDLE_INCENTIVE_EXCLUDED_HOTKEYS: str = Field(env="IDLE_INCENTIVE_EXCLUDED_HOTKEYS", default="") # DAH-2251 — referral incentive paid via emission, to referrer hotkeys weighted by # the backend's epoch-stable EMA feed. There is no funding wallet, so unlike the # earlier carve-out design this has no UID/coldkey knobs. # # The pool is `min(share * cycle total, residual burn)` and is drawn ONLY from burn # hotkeys — a share OF the total, but never taken FROM the miners. Every non-burn # miner's score is left exactly as it was, so referral emission moves value from # burn to referrers and never dilutes mining. See `_apply_referral_pool`. # # REFERRAL_EMISSION_SHARE is env-tunable and defaults to INERT (0.0): nothing is # redirected until ops sets a non-zero share at launch (coordinated with the backend # referral rail). REFERRAL_EMISSION_SHARE: float = Field(env="REFERRAL_EMISSION_SHARE", default=0.0) # DAH-2251 — fail-closed feed of epoch-stable referral EMA weights, served by the # backend at `/v1/referral-weights`. Left unset it is DERIVED # from COMPUTE_REST_API_URL (see `get_referral_feed_url`) so a staging validator # reads the staging feed; set REFERRAL_FEED_URL only to host the feed elsewhere. REFERRAL_FEED_URL: str | None = Field(env="REFERRAL_FEED_URL", default=None) # Max age (in epochs) a feed response may lag behind the validator's current epoch # before it's treated as stale and rejected (fail-closed → no referral emission). REFERRAL_FEED_MAX_STALENESS_EPOCHS: int = Field( env="REFERRAL_FEED_MAX_STALENESS_EPOCHS", default=3 ) # A cycle that a check other than VerifyX failed publishes the VerifyX EMA the backend held # before it (none for a never-measured node) instead of one moved by its sample. A passing # cycle without the recommended image cached does not seed a never-measured node; a node with # a stored EMA publishes its sample on every passing cycle. See verifyx_ema_hold_reason. # Rented cycles in a row in which the inspector saw nothing (error, failed canary, stopped or # just-started collector) are counted per node and logged as INSPECTOR_BLIND_STREAK from this # many on. INSPECTOR_BLIND_STREAK_LIMIT: int = Field(env="INSPECTOR_BLIND_STREAK_LIMIT", default=5) # On: the Inspector check fails from that cycle until the sensor reports again. Off by default. INSPECTOR_BLIND_STREAK_ENFORCEMENT_ENABLED: bool = Field( env="INSPECTOR_BLIND_STREAK_ENFORCEMENT_ENABLED", default=False ) # On a libinspector.so hash mismatch a RENTED executor, during the rental, curls # INSPECTOR_LIBRARY_FETCH_URL once into a temp file beside /usr/lib/libinspector.so, and the # file replaces the library (one rename) only if its sha256 is the validator's own. This is a # root write to /usr/lib on a renter's host, so it has its own switch, off by default and # independent of VERIFYX_LIBRARY_REFRESH_ENABLED (which covers only unrented executors). # Off stops later replacements and undoes none; restoring the replaced file (its hash is # previous_sha256 in INSPECTOR_LIBRARY_REPLACED) is in .env.template. INSPECTOR_LIBRARY_REFRESH_ENABLED: bool = Field(env="INSPECTOR_LIBRARY_REFRESH_ENABLED", default=False) # Tracks main, like verifyx.LIBRARY_FETCH_URL, so executors on older images still get the current build. INSPECTOR_LIBRARY_FETCH_URL: str = Field( default="https://raw.githubusercontent.com/Datura-ai/lium-io/main/neurons/executor/libinspector.so", description="Raw GitHub URL the executor curls when the libinspector.so refresh is on and the hash does not match", ) # DAH-5301: a reboot resets a hung pod GPU while the pod is stopped. Off = shadow: the hung GPU query runs and # the validator logs the hung GPUs, why, and whether it would reset or refuse; nothing on the host changes and # the backend gets no warning. HUNG_GPU_RESET_ON_REBOOT_ENABLED: bool = Field(env="HUNG_GPU_RESET_ON_REBOOT_ENABLED", default=False) # Emergency switch for new volumes only; existing recorded drivers remain usable. LOOPBACK_PLUGIN_V2_ENABLED: bool = Field(env="LOOPBACK_PLUGIN_V2_ENABLED", default=True) # DAH-5101: new size-limited volumes go on vloopback:v3 (direct I/O loop, no host page cache of # the volume image); off = vloopback:v2 as before. Volumes already created keep their plugin. LOOPBACK_PLUGIN_V3_ENABLED: bool = Field(env="LOOPBACK_PLUGIN_V3_ENABLED", default=False) # DAH-5101: unlink the pod's volume files before the stop so a delete does not wait for the disk # to absorb unwritten data. Off = delete exactly as before; turned on per environment. POD_VOLUME_WIPE_BEFORE_STOP_ENABLED: bool = Field(env="POD_VOLUME_WIPE_BEFORE_STOP_ENABLED", default=False) # A rental container with a memory limit runs with swap off (--memory-swap = --memory) and # RENTAL_CONTAINER_OOM_SCORE_ADJ, so a host-wide OOM picks its processes before the executor. The # memory limit stays the backend's memory_gb. 0 or below would protect the renter over host services, and Docker # refuses anything past 1000, which would fail every rental create. RENTAL_CONTAINER_OOM_SCORE_ADJ: int = Field(env="RENTAL_CONTAINER_OOM_SCORE_ADJ", default=500, ge=1, le=1000) # DAH-4537: the outside port prober (an AWS Lambda function URL) and its bearer token. Each batch # port check first fetches a few listening ports through it; when none answers it while the # validator reaches them, the node gets no verified ports. Unset URL: no outside probe. EXTERNAL_PORT_PROBE_URL: str = Field(env="EXTERNAL_PORT_PROBE_URL", default="") EXTERNAL_PORT_PROBE_TOKEN: str = Field(env="EXTERNAL_PORT_PROBE_TOKEN", default="") # Off: an outside failure is logged ("log-only"), never failed, so prod can count the nodes it would fail # (the shadow step). On: a node fails on its second outside failure in a row. EXTERNAL_PORT_PROBE_ENFORCE: bool = Field(env="EXTERNAL_PORT_PROBE_ENFORCE", default=False) # DAH-3011: a never-validated executor's FIRST verification (the express lane's, DAH-2958 — # published spec-only, never scored) proves "this GPU exists, is the model claimed, the host is # reachable and rentable"; the VRAM-filling matmul and the 128 GB RAM proof exist to make a # SCORED cycle expensive to fake, and the next scored cycle runs them anyway. Only a caller that # passes `first_pass=True` to TaskService.create_task gets the fast path (the settings below); # the wave never does, so every scored verification is unchanged. Measured (Loki, # 2–6 Sep, idle runs): matmul p50 26 s (74 s on the fresh dogfood node), VerifyX p50 78 s. # A node whose Docker cannot cap a container's disk (specs.storage_limit_supported) lets one renter fill the # host's Docker storage and take every pod down. Off: a STORAGE_LIMIT_MISSING warning only, so providers get # notice first. On: an unrented node without the limit scores 0 (StorageLimitCheck). REQUIRE_STORAGE_LIMIT_SUPPORTED: bool = Field(env="REQUIRE_STORAGE_LIMIT_SUPPORTED", default=False) # The matmul is sized from min(card VRAM, this) instead of the whole card: same challenge, # seal and UUID check, 5–17x less data to generate and copy. FIRST_PASS_MATMUL_VRAM_MB: int = Field(env="FIRST_PASS_MATMUL_VRAM_MB", default=8192) # A filler-only node skips the matmul while the filler runs. With N > 0, every Nth such cycle # runs the challenge on a small VRAM budget beside the filler and records the answer # (FILLER_MATMUL_SAMPLE); the check result never depends on it. Each sample is pinned to the # next claimed card in rotation. 0 = never. Default 96 = about once a day per node (Mikhail, DAH-4278). FILLER_MATMUL_SAMPLE_EVERY_N_CYCLES: int = Field( env="FILLER_MATMUL_SAMPLE_EVERY_N_CYCLES", default=96, ge=0 ) # Above the 2048 MB the matmul keeps in reserve, or no matrix fits. FILLER_MATMUL_SAMPLE_VRAM_MB: int = Field(env="FILLER_MATMUL_SAMPLE_VRAM_MB", default=4096, ge=4096) # VerifyX challenge config for the first pass (defaults 128 GB / 5 GB in VerifyXSettings). The # measurements are still taken and published; only the amount of RAM/disk written shrinks. FIRST_PASS_VERIFYX_MEMORY_MAX_TEST_GB: int = Field(env="FIRST_PASS_VERIFYX_MEMORY_MAX_TEST_GB", default=16) FIRST_PASS_VERIFYX_STORAGE_TEST_GB: int = Field(env="FIRST_PASS_VERIFYX_STORAGE_TEST_GB", default=1) # liumd phase 1 (DAH-2834): for an executor whose `/version` advertises `local_verify/1`, run the # capability matmul and VerifyX with ONE signed `POST /verify` instead of the SSH commands, and # judge the answer with the same unseal/verify code. The POST travels through a direct-tcpip # channel of the pipeline's SSH session to the executor's loopback port (`/version` names it); # the executor refuses any other peer. Any refusal, timeout, mismatch or local failure falls # back to the SSH path for that step (logged per outcome, `[local_verify]`). # Whole-call budget for `POST /verify` (seconds). Only the first pass calls it (a full-size VerifyX # stays on SSH), so keep this above VERIFYX first-pass p90 (90 s) + matmul. # The executor is told to stop 30 s earlier (`deadline_s` = this − 30, min 5) so a cut answer # with finished steps inside still arrives inside the budget. LOCAL_VERIFY_TIMEOUT_SECONDS: int = Field(env="LOCAL_VERIFY_TIMEOUT_SECONDS", default=240) # `GET /version` (connect, and twice this as its whole budget) and the tunnel's local bind and # connect; the direct-tcpip channel open on the executor runs inside the whole-call budget. LOCAL_VERIFY_CONNECT_TIMEOUT_SECONDS: int = Field(env="LOCAL_VERIFY_CONNECT_TIMEOUT_SECONDS", default=5) # The liumd shadow: after the pipeline has produced the node's result, run the same # GPU steps again through `liumd run` on an SSH exec channel of the same session and log one # `[liumd_shadow] comparison` line per node (per-step verdicts, agreement, durations). Nothing # it finds is scored or stored: the result is built before it starts. Off: nothing new runs. VALIDATOR_LIUMD_SHADOW: bool = Field(env="VALIDATOR_LIUMD_SHADOW", default=False) # The shadow's whole-call budget (seconds), clipped to what the executor task has left; the # intent's `deadline_s` is this − 30. Sized for a scored cycle, where VerifyX (p90 about 150 s) # and the matmul (at most 120 s) run one after the other. LIUMD_SHADOW_TIMEOUT_SECONDS: int = Field(env="LIUMD_SHADOW_TIMEOUT_SECONDS", default=330) # The only executors the shadow runs on: reserved, non-rentable dev executors, as # comma-separated `miner_hotkey/executor_uuid/address/ssh_port` entries. A pod or filler started # after the cycle's snapshot is not seen, so on a rentable node the shadow could run its # GPU/RAM/disk/network probe next to a workload. Executor UUIDs are miner-chosen, so an entry # matches only that miner's executor at that SSH endpoint. Empty = none. LIUMD_SHADOW_RESERVED_EXECUTORS: str = Field(env="LIUMD_SHADOW_RESERVED_EXECUTORS", default="") # DAH-3338: send every container state a cycle saw on a node to the backend as PodStatesReport # chunks (256 states each) after the cycle's spec, so a node with 256 rented pods still reports # its reaped orphans the same cycle. The spec keeps a bounded copy either way. Turn on once the # backend accepts the message (lium-platform#312); a backend without it logs and drops each # report. Off: the spec is the only carrier, with a floor of REAPED_POD_STATES_FLOOR slots for # reaped ids; on a node with more than 224 rented pods the last rented pods' states are cut # until the queue drains. POD_STATES_REPORT_ENABLED: bool = Field(env="POD_STATES_REPORT_ENABLED", default=False) # DAH-2671 spec-truth checks. Each pair follows the shadow-first pattern: # CHECK_ENABLED is the master switch — shadow mode computes the signal and emits the event/log # but returns passed=True and never changes score; ENFORCEMENT (only effective while # CHECK_ENABLED is on) additionally lets the signal fail closed / fail the check. No enforcement # flag defaults to True: a false positive on a scoring path zeroes an honest miner's emission, # so we shadow first and flip enforcement from clean fleet data (operator decision, not a commit). # # Item 1 — surface the kernel (procfs) GPU verdict the validator already computes and throws # away, and refuse the spoofable nvidia-smi XML fallback when it disagrees with the kernel map. # DAH-4891: fail a node whose kernel GPU list is read through a foreign mount over # /proc/driver/nvidia/gpus (KERNEL_GPU_VIEW_OVERLAID). Off: the finding is only recorded on the # banned_provider event (kernel_gpu_foreign_mounts). On only once the honest mount patterns are excluded. KERNEL_GPU_OVERLAY_ENFORCEMENT_ENABLED: bool = Field(env="KERNEL_GPU_OVERLAY_ENFORCEMENT_ENABLED", default=False) # Cross-check the reported GPU count and UUIDs against the kernel's GPU list # (/proc/driver/nvidia/gpus, the same read the ban checks make). CHECK_ENABLED evaluates and # logs a verdict every validation (check_id gpu.validate.kernel_count); a kernel list that is # present and shorter than the reported GPU count fails the validation # (GPU_KERNEL_COUNT_MISMATCH). A missing kernel list never fails. GPU_KERNEL_COUNT_CHECK_ENABLED: bool = Field(env="GPU_KERNEL_COUNT_CHECK_ENABLED", default=True) # The scrape mounts a size-limited vloopback volume the way a rental does and reports the verdict in # its SCRAPE_OK event. Kill switch: off, the scrape skips the test (baked in by FileEncryptService). VLOOPBACK_SCRAPE_CHECK_ENABLED: bool = Field(env="VLOOPBACK_SCRAPE_CHECK_ENABLED", default=True) # Item 3 — run the GPU work-proof on every claimed card concurrently (own challenge per card, # sized from the registry SKU) and time the aggregate on the validator, so a count lie either # fails device selection or serialises past a wide wall-clock threshold. # Defaults to False: "shadow" here only means the verdict is # not scored — the GPU work itself is real, so an 8-card host would run 8 near-full-VRAM matmuls # on top of the legacy single-card one every cycle. Flip it on once elapsed/VRAM/host-RSS numbers # from a real 8-GPU box show what that costs an honest miner. MATMUL_ALLCARDS_CHECK_ENABLED: bool = Field(env="MATMUL_ALLCARDS_CHECK_ENABLED", default=False) MATMUL_ALLCARDS_ENFORCEMENT_ENABLED: bool = Field(env="MATMUL_ALLCARDS_ENFORCEMENT_ENABLED", default=False) # Item 2b — send the advertised CPU-core count in the rental-verification health check so the # host's Docker daemon creates the probe with `--cpus=` and rejects a count that # exceeds the machine's physical cores (a signal the lscpu wrapper cannot forge, since dockerd # reads the kernel directly). The count is always sent (not on TDX hosts); a # CPU_QUOTA_EXCEEDS_HOST verdict zeroes the score. # Rented pod SSH, observe only: at the start of each cycle the validator # reads the SSH identification line of every rented pod listed with an ssh_port, from outside the # container (services/pod_ssh_probe.py), and reports each result with the node's result. The # score is not changed by it. TIMEOUT bounds one probe (connect and read); CONCURRENCY bounds how # many run at once, so a fleet of N pods takes at most ceil(N / CONCURRENCY) * TIMEOUT. RENTED_POD_SSH_PROBE_ENABLED: bool = Field(env="RENTED_POD_SSH_PROBE_ENABLED", default=True) RENTED_POD_SSH_PROBE_TIMEOUT_SECONDS: float = Field(env="RENTED_POD_SSH_PROBE_TIMEOUT_SECONDS", default=5.0, gt=0) RENTED_POD_SSH_PROBE_CONCURRENCY: int = Field(env="RENTED_POD_SSH_PROBE_CONCURRENCY", default=64, ge=1) # A probed rented node the cycle has no result for (the miner timed out, failed, or left it out) # is reported as EXECUTOR_RESULT_MISSING with only its observations. Off until the backend that # stores such a report as evidence only (lium-platform) is deployed: # an older backend would score it as a failed validation. RENTED_POD_SSH_RESULT_MISSING_REPORT_ENABLED: bool = Field( env="RENTED_POD_SSH_RESULT_MISSING_REPORT_ENABLED", default=False ) # A rented node whose scrape, run in the already-running executor, finds the GPU runtime # dead on the host (NVML: driver not loaded, GPU lost, GPU requires reset, GPU not found; or zero # cards) is reset the way POD_NOT_RUNNING and GPU_MISSING are: verified job cleared, so the backend # marks the executor inactive and stops billing the renter. Only the validator's scrape on the host # decides; nothing read from the renter's container does. A later clean scrape validates the node as # usual. Off (the default): the scrape failure halts the cycle without a reset, as before. On once # the matching backend change has rolled out, which handles this reset like GPU_MISSING (penalty # proposed, relisted only after a full GPU re-verification). Turning it off stops # future resets only: an executor already reset stays inactive until its next clean scrape, and # cycles already skipped are not billed or paid afterwards. RENTED_HOST_GPU_FAULT_RESET_ENABLED: bool = Field(env="RENTED_HOST_GPU_FAULT_RESET_ENABLED", default=False) # A rented pod whose reboot failed (backend status REBOOT_FAILED) and whose container is not running: # the validator asks the host to start a throwaway GPU container (the executor's own image, # `nvidia-smi -L`). If the Docker daemon or the NVIDIA runtime refuses, the host is at fault and the # verified job is cleared, so the backend marks the executor inactive and billing stops. A transport # error or timeout decides nothing. Off by default: the pod keeps the plain POD_NOT_RUNNING verdict. REBOOT_FAILED_HOST_CHECK_ENABLED: bool = Field(env="REBOOT_FAILED_HOST_CHECK_ENABLED", default=False) # DAH-4564 — read a rented pod's cpuset, CPU quota and init oom_score_adj and flag a host that # pinned or throttled the renter below 95% of its advertised cores, and zero its score. A raised # oom_score_adj is recorded only, never scored. RENTED_POD_RESOURCES_CHECK_ENABLED: bool = Field(env="RENTED_POD_RESOURCES_CHECK_ENABLED", default=True) # RentedPodNetworkCheck logs a pod's network summary as a warning for review from this count up. RENTED_POD_OUTBOUND_FANOUT_DESTINATIONS: int = Field( env="RENTED_POD_OUTBOUND_FANOUT_DESTINATIONS", default=256, gt=0 ) # The per-pod incident mark expires this long after the last cycle that saw the fault. RENTED_GPU_DROP_STATE_TTL_SECONDS: int = Field(env="RENTED_GPU_DROP_STATE_TTL_SECONDS", default=86400, gt=0) # DAH-2735 — judge an idle node's GPU by WHO holds it, not by utilization: a competitor's # rental idling on the card (Nodexo/SN106) passes every percentage gate. CHECK_ENABLED # observes and logs the verdict; ENFORCEMENT additionally zeroes the score. Enforcement # defaults off like every other money-withholding gate above: a replay over the live fleet # flags 5 nodes, and that population deserves a shadow week before the first payout is cut. # DAH-2734 — the CPU/disk twin of the gate below: a provider who mines another subnet # (SN13/Data Universe) on a listed machine takes CPU and disk from the renter the same way # a foreign GPU workload takes the card. CHECK observes and logs; ENFORCEMENT zeroes the # score. Enforcement defaults off: a provider's own nginx also burns CPU, so the floors # need a shadow week over the live fleet before the first payout is cut. # DAH-3457 (owner, 13 Sep 2026): a listed node keeps one fixed set of GPU UUIDs (the anchor). Off = a changed # set is GPU_UUID_CHANGED (verification reset, the anchor is kept) and the event's what_we_saw carries the # would-be hard decision; a missing or added card is GPU_UUID_CHANGED too now that the fingerprint check # runs before the spec check (the count change made it SPEC_CHANGED). On = a strict subset of the anchor is GPU_MISSING (score 0 # this cycle, the node returns when the full set is back); any UUID outside the anchor marks the node # "anchor broken": score 0 on every later cycle under this executor id, no re-verification; the provider # re-registers the node to list a different set. Off again = the mark stays in the record but is ignored. # Flip after reading a week of the warn-mode rows. GPU_ANCHOR_HARD_ENABLED: bool = Field(env="GPU_ANCHOR_HARD_ENABLED", default=False) # DAH-5508: an RTX 4090 reporting ~48 GB is scored as its own model "NVIDIA GeForce RTX 4090 48GB" (own VRAM # window, rate, anchor and idle tier) instead of failing the 24 GB window. Off: such a card is rejected as before. # Ships off. Turned on only by the Lium product owner, once the native verifier accepts a 48 GB 4090, the provider # preflight classifies it too, and the all-card probe is sized from the classified model. RTX_4090_48GB_MODEL_ENABLED: bool = Field(env="RTX_4090_48GB_MODEL_ENABLED", default=False) PROVIDER_SIDE_LOAD_ENFORCEMENT_ENABLED: bool = Field( env="PROVIDER_SIDE_LOAD_ENFORCEMENT_ENABLED", default=False ) # The executor's own stack is excused from the provider-side load by image digest, and any # container started from that image carries it. Cores excused that way above this number are # logged (PROVIDER_SIDE_LOAD_STACK_EXCUSE_HIGH); with the cap on, the tier then excuses nothing. PROVIDER_SIDE_LOAD_STACK_EXCUSE_CORES: float = Field( env="PROVIDER_SIDE_LOAD_STACK_EXCUSE_CORES", default=8.0, gt=0 ) PROVIDER_SIDE_LOAD_STACK_EXCUSE_CAP_ENABLED: bool = Field( env="PROVIDER_SIDE_LOAD_STACK_EXCUSE_CAP_ENABLED", default=False ) FOREIGN_GPU_WORKLOAD_ENFORCEMENT_ENABLED: bool = Field( env="FOREIGN_GPU_WORKLOAD_ENFORCEMENT_ENABLED", default=False ) # DAH-3035 — a ~6 s kernel-fault probe after the matmul: indexed/scattered access, atomics, a pointer # chase and a pinned-memory copy round-trip over a ~2 GB working set, plus NVML before/after: a rise in # uncorrected ECC or remapped rows, a pending or failed remap, or a required recovery action is a fault. # A card that faults under indexed access (Blender "Illegal address in CUDA queue", 6 Sep) passes the # sequential matmul. It always runs and logs the verdict (DAH-4257 shadow, flag removed); # ENFORCEMENT lets a fault zero the score and stays off until the owner decides. GPU_FAULT_PROBE_ENFORCEMENT_ENABLED: bool = Field(env="GPU_FAULT_PROBE_ENFORCEMENT_ENABLED", default=False) # DAH-3110 — a card NVML reports broken (uncorrected ECC since the driver loaded, a pending or failed # row remap, a required recovery action) fails the node like a GPU ban until the host resets or # replaces it. On: a broken card faults renters' jobs. Off is the kill switch back to flag-only. GPU_MEMORY_HEALTH_ENFORCEMENT_ENABLED: bool = Field( env="GPU_MEMORY_HEALTH_ENFORCEMENT_ENABLED", default=True ) # F-1599 — a 64 MB pinned-host-memory H2D/D2H round trip on every GPU of an idle node, compared on the host. # A TDX guest whose GPUs are not in CC mode passed every check yet could not DMA pinned memory (8x H200, # 13 Oct). CHECK runs it; wrong data, a copy error or a hung copy fails the node (enforced since the shadow # review of prod logs). CHECK is on (milliseconds of copy per card; a probe that cannot start is UNKNOWN, # never a fault). PINNED_MEMORY_PROBE_CHECK_ENABLED: bool = Field(env="PINNED_MEMORY_PROBE_CHECK_ENABLED", default=True) # A fault probe with no verdict passes. Five such cycles in a row on one node are logged # (GPU_FAULT_PROBE_UNKNOWN_STREAK); with this on they are judged like a fault, which fails the # node only while GPU_FAULT_PROBE_ENFORCEMENT_ENABLED is on too. Off by default. GPU_FAULT_PROBE_UNKNOWN_STREAK_ENFORCEMENT_ENABLED: bool = Field( env="GPU_FAULT_PROBE_UNKNOWN_STREAK_ENFORCEMENT_ENABLED", default=False ) # DAH-3436: the synthetic rental probe. On an idle node (no renter pod, no filler) the validator # rents the node from itself once per RENTAL_PROBE_INTERVAL_HOURS: it starts the default renter # image through the same create_container path a renter's pod takes, with a probe-owned SSH key # and the node's verified ports, waits up to RENTAL_PROBE_SSH_DEADLINE_SECONDS for sshd's banner # on the mapped port and a login (retried until that deadline), runs `nvidia-smi -L`, and tears # the container down through delete_container. # A failed step zeroes the score and clears the verified job (RENTAL_PROBE_FAILED), like the GPU # runtime quarantine, and the failed step stands in Redis until a probe passes: a later cycle with # no verdict of its own (image gone, inconclusive) fails the node again with it; a # rented node is left alone. An idle node with the image pulled is probed again every cycle until # it passes. Renters' most common complaint after GPU faults is a pod they cannot reach (SSH refused, # "error connection"), the exact path this probe walks. It rents a container on every idle node every # 6 h. Always on and enforcing. RENTAL_PROBE_INTERVAL_HOURS: float = Field(env="RENTAL_PROBE_INTERVAL_HOURS", default=6.0, gt=0) RENTAL_PROBE_SSH_DEADLINE_SECONDS: int = Field(env="RENTAL_PROBE_SSH_DEADLINE_SECONDS", default=90, gt=0) # A node whose containers cannot reach the internet passed every check. CHECK runs the rental probe's # `egress` step, which resolves and fetches pypi.org inside its renter container; ENFORCEMENT fails the # probe on it with NO_OUTBOUND_INTERNET (score 0). Slow still passes. Enforcement is off by default # until its log-only window is reviewed. Decider: taiberium; backup jam6099. NO_OUTBOUND_INTERNET_CHECK_ENABLED: bool = Field(env="NO_OUTBOUND_INTERNET_CHECK_ENABLED", default=True) NO_OUTBOUND_INTERNET_ENFORCEMENT_ENABLED: bool = Field( env="NO_OUTBOUND_INTERNET_ENFORCEMENT_ENABLED", default=False ) # ticket-0361: a node failed 16 rents in 24 h, every one a template it did not have cached; its # dockerd pulls through the mirror docker.m.daocloud.io, whose DNS lookup times out, while cached # templates start fine. RegistryPullCheck removes and pulls a digest-pinned hello-world through the # daemon (registry-mirrors apply) under a 30 s bound on idle nodes, once per INTERVAL_HOURS at a # per-node phase (RETRY_MINUTES after a failed pull, so the confirming pull comes soon). A Docker Hub # 429 is no verdict. Two failed pulls in a row (timeout, DNS error, unreachable, manifest unknown) are # the finding: a fail (REGISTRY_PULL_FAILED, score 0). A failed pull counts only if the validator # itself reaches Docker Hub. Nothing guards an outage only the nodes see (a CDN region, a shared # mirror). Enforced since the log-only window's prod logs were clean (21 Oct 2026). # DAH-3420: the host-side speed test runs on the host network; a renter's container crosses the docker bridge, # NAT and runtime and can get 1000x less (297-588 Mbps on the host, under 1 Mbps from PyPI in a container). CHECK # reads up to 16 MiB of a pinned PyPI wheel and a pinned Hugging Face file from a throwaway container on idle # nodes once per INTERVAL_HOURS and logs CONTAINER_DOWNLOAD_SPEED; two failing readings in a row (30 min apart) # are the finding and fail the node (CONTAINER_DOWNLOAD_SLOW, score 0) when both were read on the rentals network. # MIN_MBPS is a fifth of NETWORK_MIN_PACKAGE_DOWNLOAD_SPEED_MBPS (50): one cold single-stream CDN object # read 32-88 Mbps on healthy nodes (DAH-2959), so 50 would flag them; the defect is two orders of magnitude lower. CONTAINER_DOWNLOAD_SPEED_CHECK_ENABLED: bool = Field(env="CONTAINER_DOWNLOAD_SPEED_CHECK_ENABLED", default=True) CONTAINER_DOWNLOAD_SPEED_MIN_MBPS: float = Field(env="CONTAINER_DOWNLOAD_SPEED_MIN_MBPS", default=10.0, gt=0) CONTAINER_DOWNLOAD_SPEED_INTERVAL_HOURS: float = Field( env="CONTAINER_DOWNLOAD_SPEED_INTERVAL_HOURS", default=6.0, gt=0 ) # DiskSpaceCheck: free-space floor for an idle node, matching the renter listing of the same environment MIN_FREE_DISK_GB: int = Field(env="MIN_FREE_DISK_GB", default=50, ge=0) # ContainerDownloadSpeedCheck measures idle nodes only; a renter on a node listed at ~3 Gbps got # 0.7-2 MB/s and a crawling disk. RentedThroughputCheck reads the same two files from a throwaway container on a # rented node and writes 256 MiB with fsync, once per INTERVAL_HOURS (30 min retry after a failing reading). # Two failing readings in a row (a download under CONTAINER_DOWNLOAD_SPEED_MIN_MBPS, or a write under # DISK_MIN_MB_PER_S) zero the score (RENTED_THROUGHPUT_SLOW). A side whose last 2 s before the probe already # moved BUSY_MBPS (host NICs) or DISK_BUSY_MB_PER_S (docker root disk) is no verdict: the renter is using it, and it is not slow. # So is a failing side the rented pods' own counters show they used while the probe measured it (rented_throughput.DURING_*). RENTED_THROUGHPUT_CHECK_ENABLED: bool = Field(env="RENTED_THROUGHPUT_CHECK_ENABLED", default=True) RENTED_THROUGHPUT_INTERVAL_HOURS: float = Field(env="RENTED_THROUGHPUT_INTERVAL_HOURS", default=3.0, gt=0) RENTED_THROUGHPUT_DISK_MIN_MB_PER_S: float = Field(env="RENTED_THROUGHPUT_DISK_MIN_MB_PER_S", default=50.0, gt=0) RENTED_THROUGHPUT_BUSY_MBPS: float = Field(env="RENTED_THROUGHPUT_BUSY_MBPS", default=200.0, gt=0) RENTED_THROUGHPUT_DISK_BUSY_MB_PER_S: float = Field(env="RENTED_THROUGHPUT_DISK_BUSY_MB_PER_S", default=200.0, gt=0) # DAH-5244: before its port-check container (and the rented-throughput probe) the validator times `sync -f` on # the host's Docker root - the wait every `docker run` pays while a tenant's dirty pages sit on that filesystem # - and reads the Docker-root disk and host Dirty around it (services/task/host_disk.py). CHECK_ENABLED takes # the reading, logs it and sends it to the backend (specs.disk_pressure). ENFORCEMENT additionally lets a node # rented by someone else, or running our filler, keep its last verified ports and sysbox instead of a port # check into the stall (DISK_PRESSURE_MAX_DEFERRALS cycles in a row at most; the rental probe rents nothing on # such a cycle), and leaves out the rented-throughput probe container under pressure (a probe that runs under # it judges no disk side). Shadow by default: a week of readings decides the flip. DISK_PRESSURE_CHECK_ENABLED: bool = Field(env="DISK_PRESSURE_CHECK_ENABLED", default=True) DISK_PRESSURE_GATE_ENFORCEMENT_ENABLED: bool = Field(env="DISK_PRESSURE_GATE_ENFORCEMENT_ENABLED", default=False) # DAH-5320: IdleDiskWriteCheck, last in the pipeline, writes the same 256 MiB with fsync on idle and filler nodes # once a day per node (skipped while the docker root disk already moves DISK_BUSY_MB_PER_S) and logs # IDLE_DISK_WRITE with a shadow verdict against DISK_MIN_MB_PER_S. Logged only; ENFORCE_ENABLED is read by nothing # yet (the follow-up zeroes the score on a slow disk). IDLE_DISK_WRITE_CHECK_ENABLED: bool = Field(env="IDLE_DISK_WRITE_CHECK_ENABLED", default=True) IDLE_DISK_WRITE_ENFORCE_ENABLED: bool = Field(env="IDLE_DISK_WRITE_ENFORCE_ENABLED", default=False) # a node running a filler is busy from this rate of sectors read plus written on the docker root disks: a filler # moving less than DISK_BUSY_MB_PER_S would otherwise share the disk with the probe and lower the reading IDLE_DISK_WRITE_FILLER_BUSY_MB_PER_S: float = Field( env="IDLE_DISK_WRITE_FILLER_BUSY_MB_PER_S", default=5.0, gt=0 ) # A filler create runs the validator's sshd bootstrap in Lium's own image. When sshd is installed but # never runs or listens there (e.g. a host runtime that leaves /run/sshd group/world-writable), no # renter can SSH into any container on the node either. The connector stamps that per executor and # SshdBootstrapCheck fails the unrented node (SSHD_BOOTSTRAP_FAILED, score 0) until a filler create # verifies sshd again. Enforcing by default: a listed node renters cannot reach. Off = logged as # SSHD_BOOTSTRAP_FAILED_OBSERVED, score unchanged. SSHD_BOOTSTRAP_ENFORCEMENT_ENABLED: bool = Field(env="SSHD_BOOTSTRAP_ENFORCEMENT_ENABLED", default=True) REGISTRY_PULL_CHECK_ENABLED: bool = Field(env="REGISTRY_PULL_CHECK_ENABLED", default=True) REGISTRY_PULL_PROBE_INTERVAL_HOURS: float = Field( env="REGISTRY_PULL_PROBE_INTERVAL_HOURS", default=6.0, gt=0 ) REGISTRY_PULL_PROBE_RETRY_MINUTES: float = Field( env="REGISTRY_PULL_PROBE_RETRY_MINUTES", default=30.0, gt=0 ) # DAH-4124, the unrented twin of the rented-not-listed row (DAH-3558, always on since # DAH-4276): a node in the miner's known_executor_ids that is missing # from its accepted executors (the miner could not reach it to install our key) gets no # pipeline and no row, so it only "stops reporting". On, the wave writes one failed result per # such unrented node (EXECUTOR_UNREACHABLE_BY_MINER, score 0, availability error). Off = today. UNREACHABLE_EXECUTOR_REPORT_ENABLED: bool = Field( env="UNREACHABLE_EXECUTOR_REPORT_ENABLED", default=False ) SKIP_COLLATERAL_PENALTY: bool = Field(env="SKIP_COLLATERAL_PENALTY", default=True) DRY_RUN: bool = Field(env="DRY_RUN", default=False, description="Run validation without publishing scores/weights") # DAH-4001 — delayed idle settlement. off: today's behaviour, nothing reported. shadow: post every cycle's # vector and per-node rows to the backend and read its settled vector every tempo for comparison only; live # weights unchanged. enforce: submit the backend's settled vector (the cycles scored a day earlier, inactive # nodes' idle shares moved to the verified burner); with none to serve, submit this tempo's vector with its # idle moved to the burner, never an older vector. SETTLEMENT_MODE: Literal["off", "shadow", "enforce"] = Field(env="SETTLEMENT_MODE", default="off") EXECUTOR_IMAGE_REF: str = Field( env="EXECUTOR_IMAGE_REF", default="daturaai/compute-subnet-executor:latest", ) # DAH-2701 compares the executor container's image digest with EXECUTOR_IMAGE_REF's. # False (default): an OUTDATED verdict is logged with both digests and the score, the # incentive and the validation result are unchanged. True: an idle OUTDATED executor fails # validation and every OUTDATED executor earns 0. Off until nodes auto-update again # (DAH-3419): with Watchtower stalled, 99 executors earned 0 for it in one hour on 11 Sep. EXECUTOR_IMAGE_CHECK_ENFORCE: bool = Field(env="EXECUTOR_IMAGE_CHECK_ENFORCE", default=False) # DAH-3405: for this many validation cycles, counting the one in which the registry digest of # EXECUTOR_IMAGE_REF was seen to change, an executor that fails the way a watchtower restart # makes it fail (SSH gone, scrape failed, ports missing, image not yet the new one) gets no # verdict instead of a 0. 2 = the cycle the push landed in and the next (watchtower polls # every 60 s; a node that has not pulled and restarted 15 minutes later is a node whose # watchtower is not working). Cycles are counted as the validator runs them, never by the # clock. Capped at MAX_ROLLOUT_GRACE_CYCLES (2, services/executor_rollout.py): a withheld # executor publishes nothing, and the backend marks an executor inactive (with the # EXECUTOR_INACTIVE_MID_RENTAL penalty) when its row is not updated for 1 h. 0 turns it off. EXECUTOR_ROLLOUT_GRACE_CYCLES: int = Field(env="EXECUTOR_ROLLOUT_GRACE_CYCLES", default=2) # DAH-2272: when on, raise the asyncssh logger to DEBUG (debug level 2) so the # SSH handshake (banner / key exchange / auth) is logged per connection, and # emit the per-connect ``ssh_connect_phase_timing`` breakdown (see # ``services.ssh_connect_timing``). Off by default now that the slow-connect # investigation is closed — this instrumentation is kept in the tree so it can # be re-enabled without a code change by setting SSH_DEBUG_LOGGING=true (env) # for a future investigation. Lives on the main settings (not DebugSettings, # which is local-dev only) so it is configurable in staging/prod where the # real slow connects happen. SSH_DEBUG_LOGGING: bool = Field(env="SSH_DEBUG_LOGGING", default=False, description="Enable verbose asyncssh SSH handshake debug logging and per-connect phase timing") # Root log level. DEBUG brings back the per-cycle check outcomes that repeat the previous cycle # and the per-executor job-result dump after scoring; asyncssh and sqlalchemy keep their own levels. LOG_LEVEL: str = Field(env="LOG_LEVEL", default="INFO", description="Root log level (DEBUG, INFO, WARNING, ...)") # DAH-2786 — GPU power cap revert gate. When True, an unrented executor whose host put the # GPU power limit back while a Lium idle job was running, often enough inside the window, # loses the unrented rental incentive while staying active. Ships in shadow mode: unlike # DAH-2715 this gate reads provider behaviour rather than a capability, so the prod numbers # are read first. Set to True to enforce. ENABLE_UNRENTED_POWER_CAP_REVERT_LIMIT: bool = Field( env="ENABLE_UNRENTED_POWER_CAP_REVERT_LIMIT", default=False ) # DAH-4299: whether an idle provider-chosen spot node is paid is decided as for a secure node # (its GPU model is in the unrented program and every idle-pool gate passes), not by whether a # Lium filler runs on it: a filler Lium did not start is Lium's doing (Fish). Provider-caused # cases stay excluded by the shared rules. The amount is unchanged. False: paid only while a # Lium filler runs on it, as before. SPOT_IDLE_PAY_SECURE_DECISION_ENABLED: bool = Field( env="SPOT_IDLE_PAY_SECURE_DECISION_ENABLED", default=False ) # Spot rental incentive, independent of the flag above. True: a rented spot node the backend # lists in provider_spot_executor_ids earns rental incentive at 0.5 x what the same node would # earn rented as secure (0.5 = SPOT_RENTAL_INCENTIVE_FACTOR); a node that is spot as a # restriction, and the free remainder of a partially rented spot node, earn 0 as before. # False: every rented spot node earns 0, as before. ENABLE_SPOT_RENTAL_INCENTIVE: bool = Field(env="ENABLE_SPOT_RENTAL_INCENTIVE", default=False) # A Spot node's configuration filler average is used only when it was taken over at least this # many filler GPU-hours; a thinner sample reads as no average. 2 = the backend's floor for the # per-card rate the average is built from; 24 left a lone 1-GPU Spot node unpaid for its first # day of Default Job and after every restart. FILLER_REVENUE_MIN_GPU_HOURS: float = Field(env="FILLER_REVENUE_MIN_GPU_HOURS", default=2.0) # The same threshold for a secure node (the secure floor): kept at 24 GPU-hours, since its # top-up is bounded by the remaining burn allocation, not by what the fillers earned. SECURE_FILLER_REVENUE_MIN_GPU_HOURS: float = Field( env="SECURE_FILLER_REVENUE_MIN_GPU_HOURS", default=24.0 ) # True: a run below the port floor fails INSUFFICIENT_PORTS (the verdict PortCountCheck gives an # unrented node) when every pod in the batch-start rented list is stale: not running and its # rental closed (STALE_POD_NOT_RUNNING). One running pod, one still-active rental, or a pod whose # state cannot be read keeps the rented exemption. False: the run completes and its events say # the node is hidden. It stops idle pay on those nodes, so it stays off until the validator owner # turns it on. ENFORCE_PORT_FLOOR_ON_STALE_POD: bool = Field(env="ENFORCE_PORT_FLOOR_ON_STALE_POD", default=False) # collateral_deposited keeps its meaning for the backend's provider statistics and the support board: # the miner's associated EVM address owns the executor on this contract and its collateral covers # required_deposit_amount × gpu_count × COLLATERAL_DAYS. It has no score effect. The read is the contract's # storage at the finalized block through the Substrate JSON-RPC (state_getStorage), so the URL must be a # Subtensor node; it is cached per executor for COLLATERAL_STATUS_CACHE_SECONDS. SUBTENSOR_EVM_RPC_URL: str | None = Field(env="SUBTENSOR_EVM_RPC_URL", default=None) COLLATERAL_CONTRACT_ADDRESS: str = Field( env="COLLATERAL_CONTRACT_ADDRESS", default="0x8A4023FdD1eaA7b242F3723a7d096B6CC693c7C6" ) COLLATERAL_CONTRACT_VERSION: str = "1.0.2" COLLATERAL_DAYS: int = 7 COLLATERAL_STATUS_TIMEOUT_SECONDS: float = Field(env="COLLATERAL_STATUS_TIMEOUT_SECONDS", default=5.0, gt=0) COLLATERAL_STATUS_CACHE_SECONDS: int = Field(env="COLLATERAL_STATUS_CACHE_SECONDS", default=1800, ge=0) FEATURE_FLAGS: dict[str, bool] = { FeatureFlag.VERIFYX_NETWORK_VALIDATION: False, # If it's True - then bad internet connection will raise error on synthetic job } # TDX Attestation settings TDX_VERIFIER_URL: str | None = Field(env="TDX_VERIFIER_URL", default=None) ENABLE_ATTESTATION_WHITELIST: bool = Field(env="ENABLE_ATTESTATION_WHITELIST", default=False) # G4 — TCB/advisory enforcement (CVM attestation gap remediation). # Off = warn-only telemetry: violations are logged with structured reasons but do # not reject the quote. On = fail closed with AttestationError naming the value. # Also gates the minimal-G5 hooks: fail-closed on an omitted quote from a # previously-attested (CVM-ratcheted) executor, and the CVM score gate. ENABLE_TCB_ENFORCEMENT: bool = Field(env="ENABLE_TCB_ENFORCEMENT", default=False) # Comma-separated allowlists. TCB statuses other than these reject (when enforced); # advisory IDs (e.g. INTEL-SA-XXXXX) not listed here reject (when enforced). TDX_ALLOWED_TCB_STATUSES: str = Field(env="TDX_ALLOWED_TCB_STATUSES", default="UpToDate") TDX_ALLOWED_ADVISORY_IDS: str = Field(env="TDX_ALLOWED_ADVISORY_IDS", default="") # G3 — freshness/anti-replay nonce channel. When enabled the validator mints a # 32-byte challenge per attestation event, folds it into the signed SSH-key # request, and requires the executor to echo it in TDX report_data[32:64]. ENABLE_ATTESTATION_NONCE: bool = Field(env="ENABLE_ATTESTATION_NONCE", default=False) # A nonce-bound attestation event is required at most once per this interval per # miner (freshness = "bounded, provably recent", not "every wave") — GPU evidence # collection is too expensive to force fleet-wide every cycle. TDX_REATTEST_INTERVAL_SECONDS: int = Field(env="TDX_REATTEST_INTERVAL_SECONDS", default=3600) # Issued nonce validity window; a quote echoing an older nonce is stale. ATTESTATION_NONCE_TTL_SECONDS: int = Field(env="ATTESTATION_NONCE_TTL_SECONDS", default=600) # G1 — NVIDIA GPU confidential-compute attestation (validator-side verification). # Off = observe-only: nvidia_payload, when present, is verified and logged but # never affects the outcome. On = a CVM executor (verified TDX quote) must supply # GPU evidence bound to the issued nonce; any failure raises AttestationError. ENABLE_GPU_ATTESTATION_ENFORCEMENT: bool = Field( env="ENABLE_GPU_ATTESTATION_ENFORCEMENT", default=False ) # DAH-4551 - GpuAttestationIdentityCheck: each Hopper/Blackwell GPU's NVIDIA-signed identity, pinned to its # UUID both ways. Shadow by default: a mismatch or forged evidence is logged as a warning event and the node # passes. On = it fails the node (not fatal with a live rental). Unavailable evidence alone never fails it. GPU_ATTESTATION_IDENTITY_ENFORCEMENT_ENABLED: bool = Field( env="GPU_ATTESTATION_IDENTITY_ENFORCEMENT_ENABLED", default=False ) # NVIDIA's minimum VBIOS for CC in R595, per GPU model (Trusted Computing Solutions Release Notes R595, # RN-12817-001_v02, Tables 1 (SPT CC) and 3 (MPT CC) for Blackwell). Dotted hex fields; "XX" matches any value. CC_VBIOS_MINIMUMS: dict[str, str] = Field( env="CC_VBIOS_MINIMUMS", default={"B200": "97.00.E4.00.XX", "B300": "97.10.64.00.XX"} ) # On (default) = a CC multi-GPU node below the minimum fails like an identity finding. Off = shadow: only logged. CC_VBIOS_MINIMUM_ENFORCEMENT_ENABLED: bool = Field(env="CC_VBIOS_MINIMUM_ENFORCEMENT_ENABLED", default=True) # Hosted NRAS endpoint (interim verification locus; a local nv-verifier removes # this outbound SPOF before fleet enforcement — see remediation plan CD4). NVIDIA_NRAS_URL: str = Field( env="NVIDIA_NRAS_URL", default="https://nras.attestation.nvidia.com/v3/attest/gpu" ) # Optional comma-separated allowlist of NRAS `hwmodel` claims (e.g. "GH100 A01 GSP BROM"). # Empty = no hwmodel constraint. Substring-prefix match per entry. GPU_ATTESTATION_ALLOWED_HWMODELS: str = Field(env="GPU_ATTESTATION_ALLOWED_HWMODELS", default="") # N3 — rental host-key policy. Off preserves today's behavior (a non-attestation # error while building the policy falls back to unpinned SSH). On = fail closed: # no host-key policy ⇒ no rental SSH. ENABLE_RENTAL_HOSTKEY_FAIL_CLOSED: bool = Field( env="ENABLE_RENTAL_HOSTKEY_FAIL_CLOSED", default=False ) # G2 — monotonic version floor for the measured compose (runner) releases. # Whitelisted compose hashes carry a version; hashes below the floor are rejected # ("≥ approved version", newest-wins — never a rolling window). 0 accepts every # whitelisted hash (today's behavior). TDX_MINIMUM_COMPOSE_VERSION: int = Field(env="TDX_MINIMUM_COMPOSE_VERSION", default=0) def get_allowed_tcb_statuses(self) -> set[str]: return {s.strip() for s in self.TDX_ALLOWED_TCB_STATUSES.split(",") if s.strip()} def get_allowed_advisory_ids(self) -> set[str]: return {s.strip() for s in self.TDX_ALLOWED_ADVISORY_IDS.split(",") if s.strip()} def get_liumd_shadow_reserved_executors(self) -> set[tuple[str, str, str, int]]: """`LIUMD_SHADOW_RESERVED_EXECUTORS` as (miner_hotkey, executor_uuid, address, ssh_port). A malformed entry raises ValueError rather than being dropped.""" reserved = set() for entry in self.LIUMD_SHADOW_RESERVED_EXECUTORS.split(","): if not entry.strip(): continue fields = [f.strip() for f in entry.split("/")] if len(fields) != 4 or not all(fields) or not fields[3].isdigit(): raise ValueError( "LIUMD_SHADOW_RESERVED_EXECUTORS entry is not " f"miner_hotkey/executor_uuid/address/ssh_port: {entry.strip()!r}" ) hotkey, uuid, address, ssh_port = fields reserved.add((hotkey, uuid, address, int(ssh_port))) return reserved def get_allowed_gpu_hwmodels(self) -> set[str]: return {s.strip() for s in self.GPU_ATTESTATION_ALLOWED_HWMODELS.split(",") if s.strip()} def get_filler_revenue_min_gpu_hours(self, is_spot: bool) -> float: return ( self.FILLER_REVENUE_MIN_GPU_HOURS if is_spot else self.SECURE_FILLER_REVENUE_MIN_GPU_HOURS ) def get_scoring_excluded_hotkeys(self) -> set[str]: return {s.strip() for s in self.SCORING_EXCLUDED_HOTKEYS.split(",") if s.strip()} def get_idle_incentive_excluded_hotkeys(self) -> set[str]: return {s.strip() for s in self.IDLE_INCENTIVE_EXCLUDED_HOTKEYS.split(",") if s.strip()} # DAH-2958 — express lane for never-validated executors. A new node is published only by # the fleet-wide scored cycle today, so it waits for the 15-min boundary (mean 7.5 min) and # then for the slowest miner in the fleet (2–9 min): node add → AVAILABLE p50 21.4 min over # 175 onboardings, floor 8.2 min. core/express_lane.py polls the portal's # executor snapshot every TICK seconds, runs the SAME pipeline the cycle runs on executors # this validator has never published, and publishes the result spec-only (no scored_at, so # no incentive ledger row); the next cycle scores and overwrites it as today. EXPRESS_LANE_TICK_SECONDS: int = Field(env="EXPRESS_LANE_TICK_SECONDS", default=30) # Hard bound on the extra load a registration flood can add: at most this many express # verifications in flight at once (so also per tick), and per miner. EXPRESS_LANE_MAX_IN_FLIGHT: int = Field(env="EXPRESS_LANE_MAX_IN_FLIGHT", default=4) EXPRESS_LANE_MAX_IN_FLIGHT_PER_MINER: int = Field( env="EXPRESS_LANE_MAX_IN_FLIGHT_PER_MINER", default=2 ) # Validation fast path for a new node's first, unscored verification (the express lane's). # Every check still runs and decides as it does today; only the waiting changes: # - the checks with no data dependency run at once (`PipelineFactory.build_checks(fast_path=True)`: # the matmul chain beside the port/sysbox/rental-check chain, after VerifyX has measured the # network alone — the split and the order the executor's own one-call verification uses); # - the express lane ticks every EXPRESS_LANE_FAST_TICK_SECONDS and, when the miner's portal # snapshot does not list the node yet, asks again after EXPRESS_LANE_MINER_SNAPSHOT_RETRY_SECONDS # (the central miner refreshes that snapshot every 30 s); the 120-s retry stays for every other reason. # Scored cycles never take this path: the wave passes first_pass=False. Off by default. VALIDATION_FAST_PATH_ENABLED: bool = Field(env="VALIDATION_FAST_PATH_ENABLED", default=False) EXPRESS_LANE_FAST_TICK_SECONDS: int = Field(env="EXPRESS_LANE_FAST_TICK_SECONDS", default=15, gt=0) EXPRESS_LANE_MINER_SNAPSHOT_RETRY_SECONDS: int = Field( env="EXPRESS_LANE_MINER_SNAPSHOT_RETRY_SECONDS", default=35, gt=0 ) # With the fast re-ask the lane asks this many times for a node the miner did not list yet, so # the window it covers (7 × 35 s ≈ 245 s) stays at least the serial one (2 × 120 s = 240 s). EXPRESS_LANE_MINER_SNAPSHOT_MAX_ATTEMPTS: int = Field( env="EXPRESS_LANE_MINER_SNAPSHOT_MAX_ATTEMPTS", default=8, gt=0 ) # A new node whose first pass failed is run again by the lane (up to 3 runs, 60 s # apart) instead of waiting for the next cycle; off = the failed result waits for the cycle. EXPRESS_LANE_FAILED_FIRST_PASS_RETRY_ENABLED: bool = Field( env="EXPRESS_LANE_FAILED_FIRST_PASS_RETRY_ENABLED", default=True ) def express_lane_tick_seconds(self) -> int: """How often the express lane reads the portal snapshot: the fast tick with the fast path on.""" if self.VALIDATION_FAST_PATH_ENABLED: return min(self.EXPRESS_LANE_TICK_SECONDS, self.EXPRESS_LANE_FAST_TICK_SECONDS) return self.EXPRESS_LANE_TICK_SECONDS # DAH-2211 — custom-dockerfile pod build tunables (validator side). # These mirror the spec keys `features.custom_dockerfile_pod.*`; the route # is authoritative for the size cap but the validator double-checks it as # defense-in-depth. CUSTOM_DOCKERFILE_BUILD_TIMEOUT_SECONDS: int = Field( env="CUSTOM_DOCKERFILE_BUILD_TIMEOUT_SECONDS", default=1200, description="Hard timeout for `docker build` of a custom-dockerfile pod (seconds).", ) CUSTOM_DOCKERFILE_EXPORT_MIN_BYTES_PER_SECOND: int = Field( env="CUSTOM_DOCKERFILE_EXPORT_MIN_BYTES_PER_SECOND", default=10 * 1024 * 1024, gt=0, description=( "Slowest image export (DinD `docker save | docker load` onto the host) the export timeout allows " "for: the timeout is the built image's size at this rate, never below the build timeout." ), ) CUSTOM_DOCKERFILE_TOTAL_BUDGET_SECONDS: int = Field( env="CUSTOM_DOCKERFILE_TOTAL_BUDGET_SECONDS", default=3300, gt=0, description=( "Ceiling for setup + build + export of one custom-dockerfile pod. The backend fails a pod " "still PENDING after 1 h, so a size-scaled export never runs past this." ), ) CUSTOM_DOCKERFILE_MAX_BYTES: int = Field( env="CUSTOM_DOCKERFILE_MAX_BYTES", default=65_536, description="Defense-in-depth cap on dockerfile_content size; route is authoritative.", ) CUSTOM_DOCKERFILE_BUILD_CONTEXT_HOSTS: str = Field( env="CUSTOM_DOCKERFILE_BUILD_CONTEXT_HOSTS", default="", description=( "Comma-separated hosts a build_context_url may name (https only). Empty: a request that " "names a build context fails at build_context instead of building without the files." ), ) CUSTOM_DOCKERFILE_BUILD_CONTEXT_MAX_BYTES: int = Field( env="CUSTOM_DOCKERFILE_BUILD_CONTEXT_MAX_BYTES", default=100 * 1024 * 1024, gt=0, description="Largest build-context archive the validator downloads (compressed bytes).", ) CUSTOM_DOCKERFILE_BUILD_CONTEXT_MAX_UNPACKED_BYTES: int = Field( env="CUSTOM_DOCKERFILE_BUILD_CONTEXT_MAX_UNPACKED_BYTES", default=1024 * 1024 * 1024, gt=0, description="Largest total size of the files in a build-context archive.", ) # DAH-2211 — internet-enabled custom builds run inside a throwaway sysbox # Docker-in-Docker container (NOT on the host daemon) so the build can # `--network` out for deps while a build-time escape stays confined to an # unprivileged-on-host (user-namespaced) container. Egress is firewalled # host-side to block cloud metadata + RFC1918. See the DAH-2211 build flow. CUSTOM_DOCKERFILE_DIND_IMAGE: str = Field( env="CUSTOM_DOCKERFILE_DIND_IMAGE", default="daturaai/dind:0.0.3", description="Sysbox DinD image used to build custom-dockerfile pods in isolation.", ) CUSTOM_DOCKERFILE_DIND_CPUS: str = Field( env="CUSTOM_DOCKERFILE_DIND_CPUS", default="4", description="--cpus limit for the throwaway DinD build container.", ) CUSTOM_DOCKERFILE_DIND_MEMORY: str = Field( env="CUSTOM_DOCKERFILE_DIND_MEMORY", default="8g", description="--memory limit for the throwaway DinD build container.", ) RENTAL_PIDS_LIMIT_PER_CPU: int = Field( env="RENTAL_PIDS_LIMIT_PER_CPU", default=4096, ge=0, description=( "cgroup pids.max granted to a rental container per allocated CPU. 0 disables the limit " "(rollback switch). The product is clamped to RENTAL_PIDS_LIMIT_CAP; a rental with no " "per-pod CPU cap (whole host) gets RENTAL_PIDS_LIMIT_CAP. Stops a tenant fork bomb from " "exhausting the host PID space and taking the executor offline mid-rental." ), ) RENTAL_PIDS_LIMIT_CAP: int = Field( env="RENTAL_PIDS_LIMIT_CAP", default=1_048_576, gt=0, description=( "Absolute ceiling for a rental container's pids.max, and the value used when the rental " "has no per-pod CPU cap. Kept well below a host's kernel.pid_max so a fork bomb still hits " "the cgroup wall first." ), ) RENTAL_PIDS_LIMIT_HOST_MARGIN: int = Field( env="RENTAL_PIDS_LIMIT_HOST_MARGIN", default=4096, ge=0, description=( "PIDs reserved for the host's own tasks (sshd, dockerd, the executor) when a rental " "container's pids.max is clamped to the executor's actual kernel.pid_max. The container " "gets min(scaled cap, kernel.pid_max - this), so on a host with a low kernel.pid_max it " "still cannot exhaust the global PID space before hitting its own cgroup wall." ), ) RENTAL_HOST_RAM_RESERVE_GB: int = Field( env="RENTAL_HOST_RAM_RESERVE_GB", default=4, ge=0, description=( "RAM kept back for the host (sshd, dockerd, the executor) when a rental arrives with no " "memory limit: the container gets host MemTotal minus max(this, " "RENTAL_HOST_RAM_RESERVE_PERCENT), so a renter cannot exhaust the host's memory." ), ) RENTAL_HOST_RAM_RESERVE_PERCENT: float = Field( env="RENTAL_HOST_RAM_RESERVE_PERCENT", default=3.0, ge=0, lt=100, description="Percent of host RAM kept back, see RENTAL_HOST_RAM_RESERVE_GB (3 %, as the backend: DAH-5206).", ) CUSTOM_DOCKERFILE_DIND_READY_TIMEOUT_SECONDS: int = Field( env="CUSTOM_DOCKERFILE_DIND_READY_TIMEOUT_SECONDS", default=60, gt=0, description=( "Readiness budget for the inner DinD dockerd: this many one-second probes; each probe is " "bounded by min(this value, 10) s (a `docker info` that hangs counts as one not-ready " "probe). Zero or negative would cancel the only probe, so the setting refuses them at " "load time." ), ) CUSTOM_DOCKERFILE_SETUP_STEP_TIMEOUT_SECONDS: int = Field( env="CUSTOM_DOCKERFILE_SETUP_STEP_TIMEOUT_SECONDS", default=180, gt=0, description=( "Max seconds for each setup command before a custom build (sysbox preflight, DinD " "start including its image pull, IP and resolver reads, the egress firewall helper, " "the Dockerfile write; the readiness loop keeps CUSTOM_DOCKERFILE_DIND_READY_TIMEOUT_SECONDS). " "A command over the bound fails the build at its own step instead of leaving the pod PENDING." ), ) CUSTOM_DOCKERFILE_EGRESS_BLOCK_CIDRS: str = Field( env="CUSTOM_DOCKERFILE_EGRESS_BLOCK_CIDRS", default="169.254.0.0/16,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16", description=( "Comma-separated CIDRs the custom build must NOT reach (cloud metadata + " "RFC1918). Enforced host-side in the DOCKER-USER chain, scoped to the DinD " "container IP. Build still has full public-internet egress." ), ) VOLUME_MASTER_SECRET: str | None = Field(env="VOLUME_MASTER_SECRET", default=None) DEPLOY_ENV: Literal["PROD", "LOCAL", "STAGE"] = Field(env="DEPLOY_ENV", default="PROD") debug: DebugSettings = Field(default_factory=DebugSettings) verifyx: VerifyXSettings = Field(default_factory=VerifyXSettings) incentive: IncentiveConfig = Field(default_factory=IncentiveConfig) @model_validator(mode="after") def validate_volume_encryption_secret(self) -> "Settings": secret = self.VOLUME_MASTER_SECRET if secret is None or len(secret) < 32: raise ValueError("VOLUME_MASTER_SECRET is required and must be at least 32 characters") # one unit of up to 8 bytes repeated holds at most 64 bits however long it is; a random secret # of 32+ characters is never one (odds about 2^-96 for hex). A warning, not a refusal: the # remedy, a new secret, leaves every volume encrypted under the old one unreadable encoded = secret.encode() if any(encoded[period:] == encoded[:-period] for period in range(1, 9)): logger.warning("VOLUME_MASTER_SECRET repeats a unit of at most 8 bytes, so it holds at most 64 bits") return self def get_bittensor_wallet(self) -> "Wallet": if not self.BITTENSOR_WALLET_NAME or not self.BITTENSOR_WALLET_HOTKEY_NAME: raise RuntimeError("Wallet not configured") wallet = bittensor.Wallet( name=self.BITTENSOR_WALLET_NAME, hotkey=self.BITTENSOR_WALLET_HOTKEY_NAME, path=str(self.BITTENSOR_WALLET_DIRECTORY), ) wallet.hotkey_file.get_keypair() # this raises errors if the keys are inaccessible return wallet def get_redis_connection_url(self) -> str: if self.REDIS_USERNAME and self.REDIS_PASSWORD: return f"redis://{self.REDIS_USERNAME}:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{int(self.REDIS_PORT)}" return f"redis://{self.REDIS_HOST}:{int(self.REDIS_PORT)}" def get_referral_feed_url(self) -> str: """Referral-weights feed URL, derived from COMPUTE_REST_API_URL unless overridden. Derived rather than defaulted to a literal so the feed follows the deployment: a hardcoded prod URL silently pointed staging validators at prod referral data. Returns "" when neither is set, which the fail-closed client turns into no referral emission for the cycle (DAH-2251). """ if self.REFERRAL_FEED_URL: return self.REFERRAL_FEED_URL if not self.COMPUTE_REST_API_URL: return "" return f"{self.COMPUTE_REST_API_URL.rstrip('/')}/v1/referral-weights" def get_bittensor_config(self) -> bittensor.Config: # bittensor >=10.3.2 ignores argparse defaults (BT_NO_PARSE_CLI_ARGS is on by # default), so assign values directly or Subtensor silently falls back to finney config = bittensor.Config() if self.BITTENSOR_NETWORK: config.subtensor.network = self.BITTENSOR_NETWORK first_endpoint = self.get_chain_endpoints()[0] if first_endpoint.source != PUBLIC_NODE_SOURCE: config.subtensor.chain_endpoint = first_endpoint.value return config def get_chain_endpoints(self) -> list[ChainEndpoint]: """The ordered dial list: `BITTENSOR_CHAIN_ENDPOINTS` (comma-separated) or the single `BITTENSOR_CHAIN_ENDPOINT`, then the public `BITTENSOR_NETWORK` node last. A connect or read failure moves the client to the next entry; the failed entry is dialled again only after `BITTENSOR_CHAIN_ENDPOINT_RETRY_AFTER_SECONDS`.""" return chain_endpoint_candidates( chain_endpoints=self.BITTENSOR_CHAIN_ENDPOINTS, chain_endpoint=self.BITTENSOR_CHAIN_ENDPOINT, network=self.BITTENSOR_NETWORK, ) def get_debug_miner(self) -> dict: if not self.debug.MINER_ADDRESS or not self.debug.MINER_PORT: raise RuntimeError("Debug miner not configured") miner = type("Miner", (object,), {})() miner.hotkey = self.debug.MINER_HOTKEY miner.coldkey = self.debug.MINER_COLDKEY miner.uid = self.debug.MINER_UID miner.axon_info = type("AxonInfo", (object,), {})() miner.axon_info.ip = self.debug.MINER_ADDRESS miner.axon_info.port = self.debug.MINER_PORT miner.axon_info.is_serving = True return miner settings = Settings() shared_client = SharedConfigClient( api_url=f"{settings.COMPUTE_REST_API_URL}/v1/shared-config" ) def get_total_burn_emission() -> float: """Burn-emission share from shared config, guarded for on-chain use (DAH-2274). ``SharedConfigClient`` already falls back to the packaged lium-core default when the backend is unreachable, so ``shared_client.config.total_burn_emission`` is always a valid float. The only extra guard is rejecting a structurally valid but out-of-range served value (e.g. an operator typo) before it reaches on-chain weights, falling back to the lium-core default. """ value = shared_client.config.total_burn_emission if 0.0 <= value <= 1.0: return value # Lazy import avoids a circular dependency (core.utils imports settings). from core.utils import _m, get_logger get_logger(__name__).warning( _m( "total_burn_emission out of range; using lium-core default", extra={"served_value": value, "fallback": DEFAULT_SHARED_CONFIG.total_burn_emission}, ) ) return DEFAULT_SHARED_CONFIG.total_burn_emission