Skip to content

fix(validator): make gateway authoritative and peer consensus opt-in - #310

Merged
Mathis (echobt) merged 1 commit into
mainfrom
fix/centralized-gateway-weight-authority
Sep 21, 2026
Merged

Mathis (echobt) merged 1 commit into
mainfrom
fix/centralized-gateway-weight-authority

Conversation

@echobt

@echobt Mathis (echobt) commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem and behavior

Decision (project owner). Cortex runs a centralized authoritative gateway. The master gateway is the authority for weights: it serves /v1/weights/latest, /v1/weights/{epoch} and /v1/bundle/{epoch}. Validators consume those endpoints and submit the weights on-chain. Peer-root consensus between independent validators is not part of this deployment model.

Trigger. The production validator refused every tick and never submitted:

INFO validator outcome=peer_consensus_unavailable epoch=25261

Verified on-chain fact. At block 9117927, netuid 100 has 7 hotkeys holding validator_permit. In _crosscheck, others = validators - {own} is therefore never empty, so the early if not others: return True never fired and the peer-sample requirement could never be satisfied by a single-validator deployment. Reproduced identically with the new image (a7537eed) and the old image (070ad22b) — a policy mismatch, not a regression.

Previous behavior. A peer-root sample of at least min_peer_sample was an unconditional precondition for submission whenever any other permitted validator existed. Lowering --min-peer-sample to 0 could not bypass it (that path dissents PeerSampleInsufficient by design).

Resulting behavior. Validator takes peer_consensus: bool = False and the CLI gains --peer-consensus. With peer consensus off (the new default), _crosscheck returns True before it ever reads min_peer_sample. With --peer-consensus, the peer path is byte-for-byte the old policy.

What changed

  • src/cortex/validator/service.py: new documented peer_consensus constructor parameter; _crosscheck gates only the peer-sample requirement (if not self.peer_consensus or not others: return True).
  • src/cortex/validator/__main__.py: --peer-consensus opt-in flag, help text stating the deployment model, passed through to the service. --peers and --min-peer-sample behave exactly as before when the flag is on.
  • docs/BUNDLE_SPEC.md §11.3 and docs/external-miner/validators.md: deployment model stated; peer sample reclassified as opt-in hardening rather than deleted.
  • scripts/check_repo.py: rotated the docs/BUNDLE_SPEC.md frozen-spec SHA-256 pin to match the directed §11.3 edit (821a209b… → c7a43fca…). No wire format, no enum, no protocol text was changed — see "Public contract" below.

Deliberately preserved

  • Local equivocation guard. The old = self.journal.evidence.local_root(body.epoch) block and the self.journal.evidence.root(..., local=True) write run unconditionally in both models. If our own prior root for an epoch disagrees, the validator still refuses and still persists a PEER_ROOT_CONFLICT dissent. That is self-consistency, not peer consensus.
  • Frozen wire format. DissentReason is untouched: no value renumbered, removed or reordered. RootStatement/Dissent layouts, domains and the on-chain payload are unchanged.
  • The opt-in peer path. With --peer-consensus, PEER_ROOT_CONFLICT, PEER_SAMPLE_INSUFFICIENT and the min_peer_sample == 0 refusal are all still reachable and still tested.
  • Peer listener. It keeps serving this validator's signed roots to peers regardless of the flag; --peer-consensus governs only what this validator requires of others.

Tests

  • test_multi_validator_requires_metagraph_authenticated_peer_sample (4-case parametrize incl. zero_sample) — kept, now explicitly enables peer consensus via the signed_peers helper. Still asserts PEER_SAMPLE_INSUFFICIENT.
  • test_peer_equivocation_persists_both_signed_roots_and_refuses_dispatch — passes with its assertions unchanged; it uses the shared signed_peers helper, which now sets peer_consensus=True (default for that helper).
  • New test_centralized_gateway_default_submits_without_a_peer_root_sample — peer consensus left off, metagraph contains other permitted validators (validator_permits={0, 2}); asserts the real outcome is submitted, the real chain submission [(541, ((1, 65535),), 1)], the local root persisted, and no dissent.
  • New test_local_equivocation_still_refuses_dispatch_without_peer_consensus — no peers, peer consensus off, conflicting prior local root; asserts refusal and PEER_ROOT_CONFLICT.
  • signed_peers gained a peer_consensus=True knob; no existing fixture changed meaning for other tests.

Validation

Command Result Exit
uv run --no-sync ruff format --check src tests scripts 152 files already formatted 0
uv run --no-sync ruff check src tests scripts All checks passed! 0
uv run --no-sync mypy Success: no issues found in 83 source files 0
uv run --no-sync pytest -m 'not live' -q 1114 passed in 36.04s 0
uv run --no-sync python scripts/check_repo.py --final Repository contracts passed (final Python-only gate). 0
uv run --no-sync python scripts/check_deploy.py --check-examples Python images, roles, VM host and fail-closed pins validated; no services started. 0

Before the spec-pin rotation, the suite reported 2 failed, 1112 passed, both from tests/test_repo_contract.py frozen-spec hash assertions. origin/main (0874160) was verified clean in a scratch worktree (26 passed) first, confirming the two failures came from this PR's directed §11.3 edit and not from the base.

  • Ruff format and lint
  • Mypy
  • Offline test suite
  • scripts/check_repo.py --final
  • Deployment contracts when deploy files changed (no deploy files changed; ran anyway)
  • Wheel build when packaging/runtime files changed — no packaging file changed

Public contract

  • Miner documentation is updated (docs/external-miner/validators.md, docs/BUNDLE_SPEC.md §11.3)
  • No challenge content, credential, floating production image, or invented digest was added
  • Frozen protocol specifications and BASE_*/domain compatibility remain intact — DissentReason values, RootStatement/Dissent encodings and signing domains are unchanged. The §11.3 edit is prose reclassifying the peer sample as opt-in and documenting the always-on local equivocation guard; the check_repo.py pin was rotated to the new file hash because that check detects any byte change, including a directed one.

Greptile

  • Greptile reviewed this PR and findings are resolved or answered
  • If the bot was silent, I commented @greptileai review

Risk

Residual risk: a centralized gateway is a single point of authority for weights. With --peer-consensus off, nothing outside the gateway corroborates the root a validator signs and submits. A compromised or faulty gateway that produces a well-formed, correctly-signed bundle over verified trust files is submitted on-chain without independent peer corroboration. The mitigations that remain are: the independently pinned gateway public key, local owner-signed trust roots with version watermarks, full local recomputation of the final vector (Class A dissent still submits the locally recomputed vector), the owner/validator monopoly refusal, block-freshness and reorg rechecks, and the local equivocation guard. Peer cross-check remains available and unchanged for anyone running independently-operated validators: pass --peer-consensus.

Emission impact: intended and immediate — the validator resumes submitting weights instead of refusing every tick. Rollback: revert this commit, or run with --peer-consensus to restore the previous policy without a code change. No migration: no schema, wire or on-chain payload change; journals and evidence databases are compatible in both directions.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

Safe to merge.

What we checked:

  • Baseline peer-sample enforcement tests passed before the PR. T-Rex
  • The PR validation run showed all three requested runtime outcomes matched with exit code 0. T-Rex
  • The PR validation confirmed the default configuration submitted without a peer endpoint, opt-in peer-consensus refused an unavailable peer sample, and local equivocation refused submission even when peer-consensus was disabled. T-Rex
  • Focused validator fixtures passed and the command-line help exposed the opt-in peer-consensus flag. T-Rex
Summary

Peer-root consensus is now an explicit opt-in for validator deployments using the authoritative gateway. The validator continues to require peer samples when opted in and retains its local equivocation protection in both modes.

Reviews (1) · Last reviewed commit: "fix(validator): make gateway authoritati..."

Cortex runs a centralized authoritative gateway: the master gateway is the
authority for weights and a validator consumes /v1/weights/latest and submits
the verified vector on-chain. The peer-root sample in _crosscheck was an
unconditional precondition, so a single-validator deployment on a subnet that
holds other permitted validators refused every tick with
peer_consensus_unavailable and never submitted.

Add Validator(peer_consensus=False) and the --peer-consensus CLI flag. With it
off, _crosscheck returns True before it reads min_peer_sample. With it on, the
peer path is unchanged, including PEER_ROOT_CONFLICT, PEER_SAMPLE_INSUFFICIENT
and the min_peer_sample == 0 refusal.

The local equivocation guard is preserved unconditionally: a validator whose
own prior root for the epoch disagrees still refuses and dissents
PEER_ROOT_CONFLICT. DissentReason values and the wire layout are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@echobt
Mathis (echobt) merged commit 42b72d3 into main Sep 21, 2026
6 checks passed
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.

1 participant