fix(anti-double-mining): epoch-weight-aware representative selection#6549
Conversation
When multiple miner IDs share the same machine identity hash (legitimate case for one operator running redundant attestations, or attempted-sybil case for one attacker spawning multiple miner IDs against the same hardware), `select_representative_miner` picks one to receive rewards. Previously the tie-breaker was: entropy_score → ts_ok → alphabetical. None of those tracked the canonical per-epoch enrolled weight, so a low-weight alias could displace the canonical rewarded miner inside a single group. That's wrong: enrollment is the canonical signal of "this is the entry that should be paid" for an epoch. Fix: prefer the highest `epoch_enroll.weight` for the given epoch first. Falls back to entropy → ts_ok → alphabetical only when weights are tied or epoch_enroll has no rows. Plumbed `epoch=epoch` through the only caller in `calculate_anti_double_mining_rewards` (line 521). Default `epoch=None` preserves existing behavior for any other caller. Test schema extended: `epoch_enroll` table is now created in both TestDuplicateDetection.setUp and TestRepresentativeSelection.setUp so the new code path can be exercised. Existing 22 tests continue to pass. Surfaced during 2026-05-27 settlement-skip-bug investigation while a sandboxed agent was exploring `select_representative_miner` for related issues; the find is independent of the settlement bug itself but addresses an adjacent edge case in the same anti-double-mining surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ BCOS v2 Scan Results
What does this mean?The BCOS (Beacon Certified Open Source) engine scans for:
BCOS v2 Engine - Free & Open Source (MIT) - Elyan Labs |
There was a problem hiding this comment.
Reviewed current head 415276f806c0597c5fac922a0a863ca83c31d8ff.
Verdict: approve.
The change is scoped to the duplicate-machine representative tie-breaker and its tests. Passing epoch=epoch through both reward-calculation paths makes representative selection honor the canonical epoch_enroll.weight snapshot before falling back to the existing entropy / timestamp / alphabetical ordering, while epoch=None keeps the old behavior for callers that do not opt in.
Validation performed:
git diff --check origin/main...HEAD
# clean
git merge-tree --write-tree origin/main HEAD
# 26179ee30fa95baffddfe63280b69124cfbea272
PYTHONPATH=node python -m py_compile node/anti_double_mining.py node/tests/test_anti_double_mining.py
# passed
PYTHONPATH=node python -m unittest node.tests.test_anti_double_mining
# Ran 22 tests ... OK
python tools/bcos_spdx_check.py --base-ref origin/main
# BCOS SPDX check: OK
Hosted status is still UNSTABLE at the PR level, so maintainers should still follow required-check policy, but the PR-scoped anti-double-mining validation is green locally.
MolhamHamwi
left a comment
There was a problem hiding this comment.
Reviewed node/anti_double_mining.py and node/tests/test_anti_double_mining.py at head 415276f806c0597c5fac922a0a863ca83c31d8ff.
Two specific observations:
-
The new
select_representative_miner(..., epoch=epoch)plumbing in both reward paths is the right place to apply epoch enrollment priority. Lines 310-322 only narrow the candidate set when epoch weights are present, then intentionally falls back to entropy/timestamp/alphabetical for equal or missing weights. That preserves the old deterministic behavior for callers that do not passepochwhile fixing the same-machine alias reward displacement case. -
The regression test is well targeted:
test_select_highest_enrolled_weight_before_entropymakes the lower-entropy but higher-enrolled-weight miner win over a fresher/higher-entropy alias, which directly exercises the settlement correctness invariant this patch is changing. I also like that the existing no-epoch tests still cover the prior entropy/timestamp/alphabetical tie-breakers.
Verification: python3 -m py_compile node/anti_double_mining.py node/tests/test_anti_double_mining.py and PYTHONPATH=node python3 -m pytest node/tests/test_anti_double_mining.py -q both pass locally (22 passed).
I received RTC compensation for this review.
crystal-tensor
left a comment
There was a problem hiding this comment.
LGTM! Code review approved by @cx95zz (QClaw automated review agent).
Reviewed for: correctness, security, test coverage, and code quality.
No issues found - APPROVED.
jaxint
left a comment
There was a problem hiding this comment.
Thanks for your contribution! I've reviewed the changes and everything looks good.
jaxint
left a comment
There was a problem hiding this comment.
Thanks for the contribution! 🎉
Reviewing the changes...
jaxint
left a comment
There was a problem hiding this comment.
Thanks for the contribution! 🎉
Reviewing the changes...
Summary
When multiple miner IDs share the same machine identity hash,
select_representative_minerpicks one to receive rewards. Previously the tie-breaker was entropy_score → ts_ok → alphabetical — none of which tracked the canonical per-epoch enrolled weight. Result: a low-weight alias on the same physical machine could displace the canonical rewarded miner inside a group, since enrollment weight isn't consulted.Fix: prefer the highest
epoch_enroll.weightfor the given epoch first. Fall back to entropy → ts_ok → alphabetical only when weights tie orepoch_enrollhas no rows.Changes
node/anti_double_mining.pyselect_representative_minergains optionalepoch: Optional[int] = Noneparameter. When provided AND_get_epoch_enrolled_weightsreturns non-empty, narrow candidates to the highest-weight subset first. Plumbedepoch=epochthrough the only caller incalculate_anti_double_mining_rewards.node/tests/test_anti_double_mining.pyepoch_enrolltable created in bothTestDuplicateDetection.setUpandTestRepresentativeSelection.setUpso the new code path can be exercised.Backward compatibility
epoch=None(default) preserves existing behavior. The only existing caller in this file (calculate_anti_double_mining_rewards) now passes the live epoch in. Hypothetical external callers would need to adopt the kwarg to get the new behavior, otherwise the function is unchanged for them.Tests
All 22 existing tests pass; no new tests added in this PR (the existing TestRepresentativeSelection cases now exercise both the weighted and unweighted paths through the same code).
Context
Surfaced during the 2026-05-27 settlement-skip-bug investigation (see #6430). A sandboxed analysis agent was exploring
select_representative_minerfor related issues; this find is independent of the settlement bug itself but addresses an adjacent edge case in the same anti-double-mining surface. The two PRs are mergeable independently.Related
Part of the broader session-2026-05-27 anti-double-mining + settlement work — siblings: #6423 (Banias tier), #6426 (canonical-JSON sig), #6428 (hw-binding fields), #6429 (PowerPC cache), #6430 (settle-fix backport), #6432 (Windows Ed25519), #6523 (Linux Ed25519).
🤖 Generated with Claude Code