fix(claims): reject already-settled epochs in check_claim_eligibility - #7935
Conversation
check_pending_claim only matches in-flight statuses
('pending','verifying','approved'), so an epoch whose reward was
already claimed and settled (paid out) slips past the eligibility
gate and check_claim_eligibility returns eligible=True for it.
get_eligible_epochs already works around this with its own
status='settled' lookup, and a duplicate submit is ultimately
rejected by the UNIQUE(miner_id, epoch) insert constraint — but the
core eligibility check itself (also exposed via the claims API and
consumed by claims_submission) should be authoritative.
Add check_already_claimed() and reject settled epochs with reason
'already_claimed', mirroring get_eligible_epochs. Add a regression
test proving a settled claim flips eligibility from True to False.
check_pending_claim's active-only contract is left unchanged.
|
Welcome to RustChain! Thanks for your first pull request. Before we review, please make sure:
Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150) A maintainer will review your PR soon. Thanks for contributing! |
jaxint
left a comment
There was a problem hiding this comment.
Code Review
Summary
This PR adds validation to reject already-settled epochs during claim eligibility checks, preventing duplicate or invalid claims from being processed.
Changes Reviewed
- Enhanced
check_claim_eligibilityfunction with epoch settlement validation - Added logic to detect and reject claims for epochs that have already been settled
Technical Assessment
✅ Correctness: The validation correctly identifies settled epochs and prevents re-claiming.
✅ Data Integrity: This change protects against double-spending and ensures claim integrity.
✅ Error Handling: Proper rejection logic with clear error messaging for debugging.
Security Considerations
- Critical Fix: Prevents potential double-claim exploits
- Improves overall system security by validating epoch state before processing claims
Recommendation
APPROVE - This is an important security enhancement that prevents claim manipulation.
Disclosure: This review was submitted for RTC bounty compensation.
Wallet: AhqbFaPBPLMMiaLDzA9WhQcyvv4hMxiteLhPk3NhG1iG
IcanBENCHurCAT
left a comment
There was a problem hiding this comment.
LGTM — rejects already-settled epochs in claim eligibility check. ✅
|
Verified, with a scope correction. The bug is real: without a settled guard after the pending gate, The red CI is branch-staleness, not this change: the failing tests are the |
Problem
check_claim_eligibility()gates reward claims, but the only per-miner duplicate guard it runs ischeck_pending_claim(), which matchesstatus IN ('pending','verifying','approved'). That set deliberately excludes the terminalsettledstatus. So once a miner's epoch claim has been settled (paid out),check_claim_eligibility()reports that same epoch aseligible = Trueagain.You can see the codebase already knows
settled== claimed:get_eligible_epochs()runs its own extraWHERE status = 'settled'query (claims_eligibility.py ~L702) to mark those epochs as claimed. The core eligibility check doesn't, so it's not authoritative on its own — anything callingcheck_claim_eligibility()directly (it's consumed byclaims_submission.submit_claimand surfaced via the eligibility API) sees a paid epoch as still claimable.In practice a duplicate submission is still stopped downstream by the
UNIQUE(miner_id, epoch)constraint (raisesDuplicateClaimError), so this isn't a live double-pay — but the eligibility gate itself shouldn't advertise an already-paid epoch as claimable, and shouldn't lean on an insert-time integrity error for correctness.Fix
Add
check_already_claimed()(queriesstatus = 'settled', mirroring whatget_eligible_epochsalready does) and reject those epochs incheck_claim_eligibility()withreason = "already_claimed".check_pending_claim()'s active-only contract is left untouched, so the existing helper tests are unchanged.Test
Added
test_settled_claim_blocks_reclaim: a miner who is fully eligible for an epoch, after asettledclaim is recorded, flips toeligible = False, reason = "already_claimed". Verified it fails onmain(returnseligible = True) and passes with this change. Full suite: 63 passed./claim