Compare commits

...

3 Commits

Author SHA1 Message Date
satushh
0a1fb13563 lint: lint and changelog 2025-08-12 21:23:42 +05:30
satushh
e9445a7c74 core: test which would fail in the old code 2025-08-12 21:06:30 +05:30
satushh
77cf81a004 core: address overflow possibility 2025-08-12 17:16:52 +05:30
3 changed files with 177 additions and 9 deletions

View File

@@ -2,6 +2,7 @@ package altair
import (
"context"
"math/big"
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/epoch/precompute"
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers"
@@ -295,6 +296,26 @@ func AttestationsDelta(beaconState state.BeaconState, bal *precompute.Balance, v
return attDeltas, nil
}
// safeMul3Div2 returns floor((a * b * c) / (d * e)) without uint64 overflow.
func safeMul3Div2(a, b, c, d, e uint64) uint64 {
if a == 0 || b == 0 || c == 0 {
return 0
}
var A, B, C, D, E big.Int
A.SetUint64(a)
B.SetUint64(b)
C.SetUint64(c)
D.SetUint64(d)
E.SetUint64(e)
num := new(big.Int).Mul(&A, &B)
num.Mul(num, &C)
den := new(big.Int).Mul(&D, &E)
num.Quo(num, den) // floor division, matches original integer semantics
return num.Uint64()
}
func attestationDelta(
bal *precompute.Balance,
val *precompute.Validator,
@@ -317,31 +338,35 @@ func attestationDelta(
tgtWeight := cfg.TimelyTargetWeight
headWeight := cfg.TimelyHeadWeight
attDelta := &AttDelta{}
// Process source reward / penalty
// Source reward / penalty
if val.IsPrevEpochSourceAttester && !val.IsSlashed {
if !inactivityLeak {
n := baseReward * srcWeight * (bal.PrevEpochAttested / increment)
attDelta.SourceReward += n / (activeIncrement * weightDenominator)
attDelta.SourceReward += safeMul3Div2(
baseReward, srcWeight, bal.PrevEpochAttested/increment,
activeIncrement, weightDenominator)
}
} else {
attDelta.SourcePenalty += baseReward * srcWeight / weightDenominator
}
// Process target reward / penalty
// Target reward / penalty
if val.IsPrevEpochTargetAttester && !val.IsSlashed {
if !inactivityLeak {
n := baseReward * tgtWeight * (bal.PrevEpochTargetAttested / increment)
attDelta.TargetReward += n / (activeIncrement * weightDenominator)
attDelta.TargetReward += safeMul3Div2(
baseReward, tgtWeight, bal.PrevEpochTargetAttested/increment,
activeIncrement, weightDenominator)
}
} else {
attDelta.TargetPenalty += baseReward * tgtWeight / weightDenominator
}
// Process head reward / penalty
// Head reward / penalty
if val.IsPrevEpochHeadAttester && !val.IsSlashed {
if !inactivityLeak {
n := baseReward * headWeight * (bal.PrevEpochHeadAttested / increment)
attDelta.HeadReward += n / (activeIncrement * weightDenominator)
attDelta.HeadReward += safeMul3Div2(
baseReward, headWeight, bal.PrevEpochHeadAttested/increment,
activeIncrement, weightDenominator)
}
}

View File

@@ -2,6 +2,8 @@ package altair
import (
"math"
"math/big"
"math/bits"
"testing"
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/epoch/precompute"
@@ -548,3 +550,141 @@ func testStateBellatrix() (state.BeaconState, error) {
Balances: []uint64{0, 0, 0, 0},
})
}
// helper: compute the mathematically correct head reward using big.Int
func expectedHeadRewardBig(
effectiveBalance, activeCurrentEpoch, prevEpochHeadAttested uint64,
baseRewardMultiplier, headWeight, weightDenominator, increment uint64,
) uint64 {
// baseReward := (effectiveBalance / increment) * baseRewardMultiplier
effDiv := new(big.Int).SetUint64(effectiveBalance / increment)
brMul := new(big.Int).SetUint64(baseRewardMultiplier)
baseReward := new(big.Int).Mul(effDiv, brMul)
attestedDiv := new(big.Int).SetUint64(prevEpochHeadAttested / increment)
den := new(big.Int).Mul(
new(big.Int).SetUint64(activeCurrentEpoch/increment),
new(big.Int).SetUint64(weightDenominator),
)
num := new(big.Int).Mul(baseReward, new(big.Int).SetUint64(headWeight))
num.Mul(num, attestedDiv)
num.Quo(num, den) // floor division
return num.Uint64()
}
func TestAttestationDelta_HeadRewardOverflow(t *testing.T) {
cfg := params.BeaconConfig()
// Mainnet-like constants (the test does not assume exact numbers; it reads them)
increment := cfg.EffectiveBalanceIncrement // typically 1e9 (gwei)
weightDenominator := cfg.WeightDenominator // typically 64
headWeight := cfg.TimelyHeadWeight // typically 14
// Construct a pathological-but-valid config:
// 1024 validators * 10,000,000 ETH effective balance
effectiveBalance := uint64(10_000_000) * increment // 10,000,000 ETH in gwei
activeCurrent := uint64(1024) * effectiveBalance
prevHead := activeCurrent // assume full participation for simplicity
// Validator flags to ensure we take the "reward" path (no penalties)
val := &precompute.Validator{
IsActivePrevEpoch: true,
IsSlashed: false,
IsWithdrawableCurrentEpoch: false,
IsPrevEpochSourceAttester: true,
IsPrevEpochTargetAttester: true,
IsPrevEpochHeadAttester: true,
CurrentEpochEffectiveBalance: effectiveBalance,
InactivityScore: 0,
}
bal := &precompute.Balance{
ActiveCurrentEpoch: activeCurrent,
PrevEpochAttested: activeCurrent,
PrevEpochTargetAttested: activeCurrent,
PrevEpochHeadAttested: prevHead,
}
// Use a big multiplier to trigger overflow (matches the report)
const baseRewardMultiplier = uint64(200_000_000)
const inactivityDenominator = uint64(1) // irrelevant since no penalty branch is taken
const inactivityLeak = false
// In the 'older' attestationDelta this would fail in the comparison later
got, err := attestationDelta(
bal, val,
baseRewardMultiplier, inactivityDenominator,
inactivityLeak,
)
if err != nil {
t.Fatalf("attestationDelta error: %v", err)
}
// Compute mathematically correct head reward with big.Int for comparison
want := expectedHeadRewardBig(
effectiveBalance, activeCurrent, prevHead,
baseRewardMultiplier, headWeight, weightDenominator, increment,
)
if got.HeadReward != want {
t.Fatalf("HeadReward mismatch (possible overflow): got=%d want=%d", got.HeadReward, want)
}
}
// Proves that simple reordering:
// ((baseReward*headWeight)/weightDenominator) * (attested/increment) / activeIncrement
// still overflows the intermediate multiplication, and verifies that safeMul3Div2 handles it correctly.
func TestHeadReward_EarlyDivisionStillOverflows(t *testing.T) {
cfg := params.BeaconConfig()
increment := cfg.EffectiveBalanceIncrement // typically 1e9
weightDen := cfg.WeightDenominator // typically 64
headWeight := cfg.TimelyHeadWeight // typically 14
// Pathological but valid: 1024 validators @ 10,000,000 ETH effective balance
effectiveBalance := uint64(10_000_000) * increment
activeCurrent := uint64(1024) * effectiveBalance
prevHead := activeCurrent
const baseRewardMultiplier = uint64(200_000_000)
// Ground-truth (non-overflow) result using big.Int and the original single-flooring formula.
want := expectedHeadRewardBig(
effectiveBalance, activeCurrent, prevHead,
baseRewardMultiplier, headWeight, weightDen, increment,
)
// Compute the pieces of the 'naive' formula.
baseReward := (effectiveBalance / increment) * baseRewardMultiplier
// Step 1: early divide by weightDenominator
n1 := (baseReward * headWeight) / weightDen // this fits in uint64 for our numbers
// Step 2: multiply by (prevHead/increment) — this is where overflow happens
attDiv := prevHead / increment
// Detect overflow explicitly.
hi, lo := bits.Mul64(n1, attDiv)
if hi == 0 {
t.Fatalf("Expected overflow in n1*attDiv, but hi==0 (test ineffective). n1=%d attDiv=%d lo=%d", n1, attDiv, lo)
}
// Now actually do the naive formula in uint64 to show the wrong answer.
nCorrected := (n1 * attDiv) / (activeCurrent / increment)
if nCorrected == want {
t.Fatalf("Early-division formula unexpectedly matched ground truth; adjust constants.")
}
// Verify that the naive approach fails
t.Logf("Early-division overflows: got=%d want=%d (hi of n1*attDiv=%d)", nCorrected, want, hi)
// Now verify that safeMul3Div2 handles it correctly
att := prevHead / increment
denA := activeCurrent / increment
denB := weightDen
got := safeMul3Div2(baseReward, headWeight, att, denA, denB)
if got != want {
t.Fatalf("safeMul3Div2 mismatch: got=%d want=%d", got, want)
}
t.Logf("safeMul3Div2 correctly computed: %d", got)
}

View File

@@ -0,0 +1,3 @@
### Fixed
- Fixed integer overflow in attestation reward calculation that could occur with large validator sets and high reward multipliers. The `attestationDelta` function in Altair epoch processing now uses arbitrary precision arithmetic to prevent overflow when computing source, target, and head rewards, ensuring mathematically accurate results in all scenarios.