Skip to content

fix(rip-200 v2): apply the modern_x86 loyalty bonus to epoch reward weight#7972

Open
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/rip200-v2-loyalty-bonus-never-applied
Open

fix(rip-200 v2): apply the modern_x86 loyalty bonus to epoch reward weight#7972
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/rip200-v2-loyalty-bonus-never-applied

Conversation

@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor

Wiring status first (so it's weighed correctly)

This fixes node/rip_200_round_robin_1cpu1vote_v2.py, which nothing currently imports — the live node (node/rustchain_v2_integrated_v2.2.1_rip200.py) and node/claims_eligibility.py both import the v1 module, whose get_time_aged_multiplier has no loyalty concept at all. So this is not a live payout bug and no miner is losing RTC today; it's the v2 module failing its own documented contract before anyone wires it up. Flagging that up front rather than letting it ride under a scarier title.

Problem

calculate_epoch_rewards_v2 computes the loyalty-aware multiplier and then throws it away:

base_mult = get_device_multiplier(device_info, db_path, miner_id)   # dead store
weight    = get_time_aged_multiplier(arch, chain_age_years, device_info)

get_time_aged_multiplier internally calls get_device_multiplier(device_info) without db_path/miner_id, so the if db_path and miner_id: guard is always false and get_loyalty_bonus() is unreachable from the reward path. Every modern_x86 miner is weighted at the flat 0.1 base.

That contradicts the module's own docstring ("Modern x86 (<5 years): Starts at 0.1x, earns 15%/year loyalty bonus") and its __main__ banner ("Loyalty bonuses do NOT decay"). The dead base_mult store is what convinced me this is an oversight and not a deliberate design choice — the intent to use the loyalty-aware value is right there.

Concretely: a miner with 4 years of attestation history should weigh 0.70 against a brand-new miner's 0.10 — 87.5% / 12.5% of the epoch. Instead both weigh 0.10 and split it 50/50.

Fix

Thread db_path/miner_id through get_time_aged_multiplier as optional args (default None, so every existing caller — including v1-style two-arg calls — is unaffected) and drop the dead store.

The (device_arch or "") hunk is load-bearing, not cosmetic: with loyalty applied, base_multiplier clears 0.1 and execution reaches device_arch.lower() for the first time. calculate_epoch_rewards_v2 passes the raw nullable device_arch column, so without that guard the fix would introduce an AttributeError on NULL-arch miners. There's a test pinning it.

Tests — tests/test_rip200_v2_loyalty_bonus_reward_weight.py

On current main:

FAILED test_loyalty_bonus_is_honoured_in_epoch_reward_weight
E   AssertionError: loyal miner must earn more than a brand-new miner, got veteran=750000 rookie=750000
1 failed, 2 passed

With the fix: 3 passed.

The two that pass on main are deliberate no-regression guards, so they hold on both sides:

  • test_vintage_weights_unchanged — a G4's decayed 2.5x weight is asserted against the decay formula through the public reward path; loyalty must not perturb it.
  • test_null_device_arch_does_not_crash — covers the hunk above.

The failing test's precondition (get_device_multiplier(..., db, "veteran") == 0.70) passes on main, which is the proof that the loyalty machinery works and is simply never invoked.

Regression check: tests/test_epoch_window_consistency.py, node/tests/test_time_aged_multiplier_penalty.py, node/tests/test_arch_multiplier_guard_t32.py26 passed.

RTC payout address: RTCd1554f0f35576faf01d386a6be1c947f560dd0b7

…eight

calculate_epoch_rewards_v2 computed the loyalty-aware multiplier into base_mult
(passing db_path + miner_id) and then discarded it, taking the weight from
get_time_aged_multiplier() instead -- which called get_device_multiplier() without
db_path/miner_id, so the 'if db_path and miner_id' guard was always false and
get_loyalty_bonus() was never reached. Every modern_x86 miner was weighted at the
flat 0.1 base, dropping the 15%/year loyalty bonus the module documents.

Thread db_path/miner_id through get_time_aged_multiplier (optional args, existing
callers unaffected) and drop the dead base_mult store. Tolerate a NULL device_arch
in the decay branch: calculate_epoch_rewards_v2 passes the raw nullable column, and
that branch is only reachable once loyalty lifts the base above 0.1.
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to RustChain! Thanks for your first pull request.

Before we review, please make sure:

  • Non-doc PRs have a BCOS-L1 or BCOS-L2 label
  • Doc-only PRs are exempt from BCOS tier labels when they only touch docs/**, *.md, or common image/PDF files
  • New code files include an SPDX license header
  • You've tested your changes against the live node

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!

@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) node Node server related tests Test suite changes size/M PR: 51-200 lines labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) node Node server related size/M PR: 51-200 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant