fix(validator): refuse owner-burn and unsealed LKG submit - #208
Conversation
UID 0 is the owner+validator hotkey, so a sealed uid0=100% vector is a payout, not a burn. Skip Match submit when one UID holds all mass and is SubnetOwnerHotkey or validator_permit. Unsealed latest is not a submit path: do not load or submit LKG. HTTP burn-uid0.v1 fallback is unchanged. Co-authored-by: Mathis <echobt@users.noreply.github.com>
maybe_submit_match exceeded clippy::too_many_lines; record_submit_outcome keeps the Match submit path under the 100-line cap. Co-authored-by: Mathis <echobt@users.noreply.github.com>
|
@greptileai review |
Greptile SummaryThis change adds sealed-vector safeguards so validators avoid submitting unsealed results and pure payouts to subnet owners or validator-permit UIDs. Focused execution reproduced two paths that bypass those safeguards: unreadable permit storage is treated as an empty permit map, and an unreadable seal-time metagraph is replaced with mutable tip state. Both cases can submit a sealed pure payout that should be skipped. Merge safety: do not merge until permit-state uncertainty and unavailable historical seal snapshots both fail closed. Confidence Score: 3/5Unsafe to merge because two independently reproduced payout-guard bypasses can submit sealed vectors that should be rejected. Focused Rust tests exercised both failure paths through the submission flow: malformed permit data produced an empty permit map and a recorded submission, while an unavailable historical snapshot changed a skipped payout into a submitted payout. Files Needing Attention:
What T-Rex did
|
| match raw { | ||
| Ok(Some(bytes)) => storage::decode_vec_bool(&bytes).unwrap_or_default(), | ||
| Ok(None) | Err(_) => Vec::new(), |
There was a problem hiding this comment.
Permit-read failures bypass the payout guard
RPC failures, missing storage, and decode failures for ValidatorPermit are all converted into an empty vector. The sealed Match guard then treats the target UID's absent permit entry as a definite non-permit result, allowing a pure payout to a validator-permit hotkey to reach submit_intent. Preserve whether permit data was read successfully and skip submission whenever the target UID's permit status cannot be established.
Artifacts
Review-authored chain-live malformed ValidatorPermit test source
- The captured review-authored wiremock test returns malformed ValidatorPermit SCALE storage and asserts the live metagraph still loads with no permit entries, proving the first half of the bypass.
Review-authored validator pure-payout submission test source
- The captured review-authored validator test models the empty permit map after the read fault and asserts a sealed pure payout to UID 1 records one submission, proving the guard can consequently submit.
Chain-live malformed ValidatorPermit test output
- This complete cargo-test output records the command, working directory, and exit code 0 for the wiremock metagraph test, showing malformed permit storage becomes an empty permit map.
Validator pure-payout submission test output
- This complete cargo-test output records the command, working directory, and exit code 0 for the submission-path test, showing the empty permit map permits one recorded submission.
- This command capture records hashes and line counts for the two uploaded review-authored source files, tying the executed proof to the uploaded sources.
| if let Some(block) = metagraph_block { | ||
| if let Ok(hash) = chain.block_hash(block) { | ||
| if let Ok(mg) = chain.metagraph_at(&hash) { | ||
| return Ok(mg); | ||
| } | ||
| } | ||
| } | ||
| let tip = chain.current_block()?; | ||
| let hash = chain.block_hash(tip)?; | ||
| chain.metagraph_at(&hash) | ||
| } |
There was a problem hiding this comment.
Seal snapshot fallback misclassifies payouts
When a bundle supplies metagraph_block, an error fetching that historical snapshot is silently replaced with the current-tip metagraph. Owner and validator-permit status may have changed since sealing, so a pure payout that the seal-time guard would skip can be classified with newer state and submitted. Propagate the historical lookup failure when a seal block is present, and use the tip only when no seal block was supplied.
Artifacts
- Source of the temporary focused review test fixture and paired historical/pruned cases; it shows the exact `ChainClient` setup used to force the fallback and record the submission call.
Historical seal snapshot control run
- Executed historical-snapshot control run; it reports `submission_calls=0`, proving the pure payout is skipped when the seal metagraph is readable.
Unavailable seal snapshot fallback run
- Executed unavailable-snapshot fallback run; it reports `submission_calls=1`, proving fallback to the changed tip submits the sealed payout.
primeorder 0.14.0 (via p256/dcap-qvl) pulled wnaf 0.14.0, yanked today in favor of 0.14.1. Smallest lockfile bump to keep cargo-deny green. Co-authored-by: Mathis <echobt@users.noreply.github.com>
Summary
Live
GET /v1/weights/latestcan be a sealedburn-uid0.v1vectoruids:[0] weights:[1.0]. UID 0 is the subnet owner+validator hotkey, so Yuma pays the owner — that is not a burn.This PR only stops paying the owner. It does not register a discarded-key burn neuron, pin
burn_uid, or bumpalgorithm_version/burn-eligible-v1(follow-up). Frozen BUNDLE_SPEC §6 vectors /protocol_version/algorithm_versionare unchanged.apply_unsealed_latestlogsvalidator_latest_unsealedand returnsNone. It does not load LKG, compare, or submit.lkg.savestill runs on sealed Match (disk ok; submit forbidden whilelatest.sealed=false).latest.sealed==true, outcome is Match, and the vector is notis_burn_to_registered_owner: one UID holds all non-zero mass and that UID’s hotkey isSubnetOwnerHotkeyor holdsvalidator_permitat the seal metagraph. Covers sealed zero-miner{0: 1.0}to owner. On hit: no submit, logvalidator_submit_skipped_owner_burn, no new dissent code.build_burn_fallbackstill serves uid0=1.0sealed:falseso/latestnever 404s. Validators must not Match/submit it; submittingburn-uid0.v1pays the owner.algorithm_version/protocol_versionare not bumped.wnaf0.14.0 → 0.14.1 (cargo update -p wnaf). Same cargo-deny yanked-crate fix as feat(proof): Proof challenge + dynamic topics + RLM judge digest pin #209. No product-scope change.Greptile
Every PR is reviewed by Greptile before merge. Config:
.greptile/.@greptileai reviewTest plan
cargo test -p validator -p chain -p chain-live -p weights-api -p challenge-commoncargo fmt --all -- --check-D warnings;maybe_submit_matchsplit so it stays undertoo_many_lines)cargo run -p xtask -- spec-check/loc-cap/external-docs-check/design-check/consensus-lintcargo deny check(local, afterwnaf0.14.1)Risk
Validators that previously submitted LKG on gateway burn-fallback, or submitted a sealed uid0=100% owner vector, will stop submitting until a sealed non-owner vector exists. Chain may remain at owner 100% until a real miner vector is sealed (or a later eligible-sink PR). HTTP
/lateststill never 404s.Naming
I did not rename
BASE_*environment variables, deployed host paths(
/opt/base,/run/base, …), GHCRbaseintelligence/basepackage names, orbase-*-v1cryptographic domain tags, unless this PR’s purpose is a coordinatedcutover documented in
docs/NAMING.md.