Skip to content

Feature: Manual/Automated Switch to Burn Mode for Exploit Detection#78

Open
machine-god-deus wants to merge 1 commit into
mainfrom
agent/issue-77-feature-manual-automated-switch-to-burn-mode-for-exploit-detection
Open

Feature: Manual/Automated Switch to Burn Mode for Exploit Detection#78
machine-god-deus wants to merge 1 commit into
mainfrom
agent/issue-77-feature-manual-automated-switch-to-burn-mode-for-exploit-detection

Conversation

@machine-god-deus

@machine-god-deus machine-god-deus commented Apr 4, 2026

Copy link
Copy Markdown

Summary

  • Add runtime-toggleable burn mode to redirect emissions away from exploitative miners when skip-backward or rebinding exploits are detected
  • New BurnMode Pydantic model in src/crusades/chain/burn_mode.py with enabled, burn_rate_override, blocked_uids, reason, activated_at, activated_by fields
  • Persistence via existing validator_state KV store: get_burn_mode() and set_burn_mode() added to src/crusades/storage/database.py
  • WeightSetter.set_weights() in src/crusades/chain/weights.py loads burn mode state at cycle start; when active, overrides burn_rate from hparams and redirects blocked UID emissions to burn_uid
  • Three new HMAC-authenticated API endpoints on the existing FastAPI server in src/crusades/api/server.py: POST /burn-mode/activate, POST /burn-mode/deactivate, GET /burn-mode/status
  • 20 new tests across three files covering model validation, DB persistence, API auth/activation/deactivation, and weight distribution behavior

Validation

  • uv run pytest tests/ -v — 20/20 passed
  • uv run ruff check — all checks passed
  • Unit tests confirm BurnMode serialization round-trip, validation bounds on burn_rate_override, and inactive() factory default
  • DB tests verify set_burn_mode() / get_burn_mode() persistence and correct fallback to inactive state when no prior state exists
  • API integration tests (via httpx.AsyncClient) cover auth enforcement (401 without key), activation/deactivation lifecycle, status reflection, and input validation (422 on out-of-range burn rate)
  • Weight integration tests confirm burn rate override applies when active, blocked UIDs are redirected to burn_uid, and inactive burn mode falls back to hparams

Risks

  • Operator error activating burn mode with overly broad blocked_uids could suppress emissions for honest miners; mitigation is the reason field and GET /status endpoint for auditability
  • No automatic deactivation or TTL — burn mode stays active across restarts until explicitly deactivated

Closes #77

Summary by CodeRabbit

Release Notes

  • New Features

    • Added burn mode API endpoints to activate, deactivate, and retrieve current burn mode status
    • Burn mode allows overriding burn rates (0-100%) and blocking specific validators
    • Burn mode activation requires API key authentication and includes audit tracking with reason and timestamp
    • Integrated burn mode into weight-setting logic to enforce overrides and blocked validator redirects
  • Tests

    • Added comprehensive test coverage for burn mode model, database persistence, API endpoints, and weight-setting integration

@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown

Walkthrough

This pull request introduces a "burn mode" feature for emergency emission control and exploit detection. It adds a BurnMode Pydantic model, persists it via the database, exposes management endpoints via a new /burn-mode API router, and integrates the mode into the weight-setting logic to conditionally override burn rates or block specific UIDs.

Changes

Cohort / File(s) Summary
BurnMode Model
src/crusades/chain/burn_mode.py
Introduces BurnMode Pydantic model with fields for enabled status, burn_rate_override (constrained to [0.0, 1.0]), blocked_uids, reason, activated_at, and activated_by. Includes inactive() classmethod and BURN_MODE_KEY constant.
Database Persistence
src/crusades/storage/database.py
Adds get_burn_mode() and set_burn_mode() methods to store and retrieve burn mode state via the validator_state KV store using JSON serialization.
API Router & Server Lifecycle
src/crusades/api/server.py
Introduces /burn-mode router with activate, deactivate, and status endpoints. Activate validates request body, persists BurnMode via database. Deactivate sets inactive state. Status fetches current mode. Updates CORS to allow POST and registers router in create_app(). Adds SQLite database initialization on startup and cleanup on shutdown.
WeightSetter Integration
src/crusades/chain/weights.py
Loads burn mode before syncing metagraph. When enabled, uses burn_rate_override instead of hparams burn_rate. Blocks winner UIDs listed in blocked_uids by redirecting emissions to burn_uid early return.
BurnMode Model Tests
tests/test_burn_mode.py
Tests BurnMode.inactive() factory, field validation (especially burn_rate_override boundary checks), JSON serialization/deserialization roundtrips, and database persistence via async in-memory SQLite fixture.
API Integration Tests
tests/test_burn_mode_api.py
Tests activate, deactivate, and status endpoints with authentication via API key validation. Covers successful activation, rejection without API key (401), field validation (422), default values, persistence verification, and status endpoint state reflection.
WeightSetter Integration Tests
tests/test_burn_mode_weights.py
Tests WeightSetter.set_weights() behavior with burn mode enabled, including burn_rate_override application, blocked UID triggering, and fallback to hparams when inactive. Uses mocked chain manager and database fixtures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Burn mode, oh burn mode, let the flames rise high!
Block the bad actors and let honest miners fly,
With override rates and UIDs on the list,
Exploit detection—no bad actors missed! 🔥

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature added: a burn mode mechanism for exploit detection that can be toggled manually/automatically.
Linked Issues check ✅ Passed The implementation comprehensively addresses all three objectives from issue #77: emergency response via burn rate override, targeted action through blocked_uids, and transparency through reason field and status endpoint.
Out of Scope Changes check ✅ Passed All changes directly support burn mode functionality: new BurnMode model, database persistence, API endpoints, weight distribution logic, and corresponding tests. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/issue-77-feature-manual-automated-switch-to-burn-mode-for-exploit-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/crusades/api/server.py`:
- Around line 110-145: The burn-mode endpoints (burn_mode_router and handlers
activate_burn_mode / deactivate_burn_mode) must require a configured dashboard
API key instead of inheriting the "open if no key" behavior; add an
authorization dependency that enforces a non-empty DASHBOARD_API_KEY (e.g.
create a require_dashboard_api_key dependency that validates
Request.app.state.config.DASHBOARD_API_KEY and the incoming key) and attach it
to the router (APIRouter(..., dependencies=[Depends(require_dashboard_api_key)])
or to each endpoint), or alternatively add a startup check that raises an
exception when DASHBOARD_API_KEY is missing so the app fails to start; ensure
the dependency name and router/handler symbols (burn_mode_router,
activate_burn_mode, deactivate_burn_mode) are used so the protection covers
these routes.
- Around line 72-76: The code caches a MockClient via get_db_client() before the
real SQLite DB is created, causing the mock to persist; to fix, create and
initialize the real Database (use Database, db_path, await db.initialize(), and
assign app.state.db) before any call to get_db_client() or before caching the
dashboard client so get_db_client() will detect the real DB and return the real
client instead of MockClient. Ensure the Database initialization block runs
prior to whatever logic caches the dashboard client or calls get_db_client().

In `@src/crusades/chain/burn_mode.py`:
- Around line 15-20: BurnMode currently allows enabled=True with an empty
reason; add a validation rule so enabling burn mode requires a non-empty reason.
In the BurnMode model add a Pydantic validator (e.g., a `@root_validator` or
`@validator`('reason', always=True)) that checks if values.get("enabled") is True
and (reason is None or reason.strip() == ""), and raise a ValueError like
"reason required when enabled is True" to prevent constructing or persisting an
enabled BurnMode without audit context; update any tests or callers that
construct BurnMode to provide a reason when enabled.

In `@src/crusades/chain/weights.py`:
- Around line 213-221: Filter out burn_mode.blocked_uids before selecting or
promoting a winner so a blocked submission never becomes _previous_winner or
influences the adaptive threshold; change the winner selection flow in the
relevant function to choose the next eligible miner when the chosen winner_uid
is in burn_mode.blocked_uids (do not immediately call _set_burn_only_weights or
burn the full share), and only call self._set_burn_only_weights(f"...") when no
eligible winner remains after filtering; ensure any state updates that ratchet
adaptive thresholds happen after this eligibility check and use the final
elected winner UID.

In `@tests/test_burn_mode.py`:
- Around line 39-45: The tests test_burn_rate_bounds_low and
test_burn_rate_bounds_high use pytest.raises(Exception) which is too broad;
update them to assert the specific validation error raised by BurnMode when
burn_rate_override is out of range (e.g., ValueError or the project-specific
ValidationError used by BurnMode), and optionally use the match= parameter to
verify the error message mentions "burn_rate_override" or "out of range"; change
the pytest.raises(Exception) calls in those two tests to
pytest.raises(<specific-exception>) (and add match="burn_rate" or similar if you
want to assert the message).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15de79e6-5d75-49bc-b1d3-239e430d3f17

📥 Commits

Reviewing files that changed from the base of the PR and between 650d166 and 09207c0.

📒 Files selected for processing (8)
  • src/crusades/api/server.py
  • src/crusades/chain/burn_mode.py
  • src/crusades/chain/weights.py
  • src/crusades/storage/database.py
  • tests/__init__.py
  • tests/test_burn_mode.py
  • tests/test_burn_mode_api.py
  • tests/test_burn_mode_weights.py

Comment on lines +72 to +76
# Initialize async database for burn mode operations
db_path = os.getenv("CRUSADES_DB_PATH", "crusades.db")
db = Database(url=f"sqlite+aiosqlite:///{db_path}")
await db.initialize()
app.state.db = db

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Initialize the real DB before caching the dashboard client.

On a first boot, get_db_client() above still sees no database file and permanently caches MockClient. This block then creates the real SQLite DB, so burn-mode writes hit the real database while the rest of the API keeps serving mock data until restart.

💡 Simple fix
     logger.info("Crusades API starting...")
-    get_db_client()
-
     # Initialize async database for burn mode operations
     db_path = os.getenv("CRUSADES_DB_PATH", "crusades.db")
     db = Database(url=f"sqlite+aiosqlite:///{db_path}")
     await db.initialize()
     app.state.db = db
+    get_db_client()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/crusades/api/server.py` around lines 72 - 76, The code caches a
MockClient via get_db_client() before the real SQLite DB is created, causing the
mock to persist; to fix, create and initialize the real Database (use Database,
db_path, await db.initialize(), and assign app.state.db) before any call to
get_db_client() or before caching the dashboard client so get_db_client() will
detect the real DB and return the real client instead of MockClient. Ensure the
Database initialization block runs prior to whatever logic caches the dashboard
client or calls get_db_client().

Comment on lines +110 to +145
burn_mode_router = APIRouter(prefix="/burn-mode", tags=["burn-mode"])


@burn_mode_router.post("/activate")
async def activate_burn_mode(
request: Request,
body: BurnModeActivateRequest,
) -> dict[str, Any]:
"""Activate burn mode. Overrides hparams burn_rate and optionally blocks UIDs."""
db: Database = request.app.state.db
burn_mode = BurnMode(
enabled=True,
burn_rate_override=body.burn_rate_override,
blocked_uids=body.blocked_uids,
reason=body.reason,
activated_at=datetime.now(UTC),
activated_by="api",
)
await db.set_burn_mode(burn_mode)
logger.warning(
"Burn mode ACTIVATED: rate=%.0f%%, blocked=%s, reason=%s",
burn_mode.burn_rate_override * 100,
burn_mode.blocked_uids,
burn_mode.reason,
)
return burn_mode.model_dump(mode="json")


@burn_mode_router.post("/deactivate")
async def deactivate_burn_mode(request: Request) -> dict[str, Any]:
"""Deactivate burn mode. Restores normal hparams-driven weight distribution."""
db: Database = request.app.state.db
burn_mode = BurnMode.inactive()
await db.set_burn_mode(burn_mode)
logger.warning("Burn mode DEACTIVATED")
return {"status": "deactivated"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

These control routes need mandatory auth, not optional auth.

These handlers inherit the dashboard's "open if no key is configured" behavior. That's fine for read-only stats, but it makes burn-mode activation/deactivation publicly writable whenever DASHBOARD_API_KEY is missing or empty, which lets anyone who can reach the API suppress emissions or block UIDs. Require a configured key for this router or fail startup when it's absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/crusades/api/server.py` around lines 110 - 145, The burn-mode endpoints
(burn_mode_router and handlers activate_burn_mode / deactivate_burn_mode) must
require a configured dashboard API key instead of inheriting the "open if no
key" behavior; add an authorization dependency that enforces a non-empty
DASHBOARD_API_KEY (e.g. create a require_dashboard_api_key dependency that
validates Request.app.state.config.DASHBOARD_API_KEY and the incoming key) and
attach it to the router (APIRouter(...,
dependencies=[Depends(require_dashboard_api_key)]) or to each endpoint), or
alternatively add a startup check that raises an exception when
DASHBOARD_API_KEY is missing so the app fails to start; ensure the dependency
name and router/handler symbols (burn_mode_router, activate_burn_mode,
deactivate_burn_mode) are used so the protection covers these routes.

Comment on lines +15 to +20
enabled: bool = False
burn_rate_override: float = Field(default=1.0, ge=0.0, le=1.0)
blocked_uids: list[int] = Field(default_factory=list)
reason: str = ""
activated_at: datetime | None = None
activated_by: str = ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't allow enabled burn mode without audit context.

BurnMode(enabled=True) is currently valid with an empty reason, so callers can persist an emergency override with no accountable explanation. For a control path that's meant to justify operator action, require a non-empty reason whenever burn mode is enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/crusades/chain/burn_mode.py` around lines 15 - 20, BurnMode currently
allows enabled=True with an empty reason; add a validation rule so enabling burn
mode requires a non-empty reason. In the BurnMode model add a Pydantic validator
(e.g., a `@root_validator` or `@validator`('reason', always=True)) that checks if
values.get("enabled") is True and (reason is None or reason.strip() == ""), and
raise a ValueError like "reason required when enabled is True" to prevent
constructing or persisting an enabled BurnMode without audit context; update any
tests or callers that construct BurnMode to provide a reason when enabled.

Comment on lines +213 to +221
# If burn mode is active and winner is blocked, redirect to burn
if burn_mode.enabled and winner_uid in burn_mode.blocked_uids:
logger.warning(
"Winner UID %d is blocked by burn mode — redirecting 100%% to burn_uid",
winner_uid,
)
return await self._set_burn_only_weights(
f"Winner UID {winner_uid} blocked by burn mode"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Filter blocked winners out before winner-state updates.

By the time this branch runs, the blocked submission may already have been promoted into _previous_winner and used to ratchet the adaptive threshold. It also burns the full winner share instead of passing it to the next eligible miner, so honest miners lose emissions even in the "targeted" path. Exclude blocked UIDs before winner selection/state updates, and only fall back to burn-only if no eligible winner remains.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/crusades/chain/weights.py` around lines 213 - 221, Filter out
burn_mode.blocked_uids before selecting or promoting a winner so a blocked
submission never becomes _previous_winner or influences the adaptive threshold;
change the winner selection flow in the relevant function to choose the next
eligible miner when the chosen winner_uid is in burn_mode.blocked_uids (do not
immediately call _set_burn_only_weights or burn the full share), and only call
self._set_burn_only_weights(f"...") when no eligible winner remains after
filtering; ensure any state updates that ratchet adaptive thresholds happen
after this eligibility check and use the final elected winner UID.

Comment thread tests/test_burn_mode.py
Comment on lines +39 to +45
def test_burn_rate_bounds_low(self):
with pytest.raises(Exception):
BurnMode(burn_rate_override=-0.1)

def test_burn_rate_bounds_high(self):
with pytest.raises(Exception):
BurnMode(burn_rate_override=1.1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Narrow the exception assertion in the bounds tests.

pytest.raises(Exception) will also pass on unrelated construction errors, so these tests can go green for the wrong reason. Assert the specific validation failure instead.

🧰 Tools
🪛 Ruff (0.15.9)

[warning] 40-40: Do not assert blind exception: Exception

(B017)


[warning] 44-44: Do not assert blind exception: Exception

(B017)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_burn_mode.py` around lines 39 - 45, The tests
test_burn_rate_bounds_low and test_burn_rate_bounds_high use
pytest.raises(Exception) which is too broad; update them to assert the specific
validation error raised by BurnMode when burn_rate_override is out of range
(e.g., ValueError or the project-specific ValidationError used by BurnMode), and
optionally use the match= parameter to verify the error message mentions
"burn_rate_override" or "out of range"; change the pytest.raises(Exception)
calls in those two tests to pytest.raises(<specific-exception>) (and add
match="burn_rate" or similar if you want to assert the message).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Manual/Automated Switch to Burn Mode for Exploit Detection

1 participant