feat(consensus): block reward with size penalty
BaseReward returns premine at genesis, fixed 1 LTHN otherwise. BlockReward applies the C++ size penalty using 128-bit arithmetic. MinerReward handles pre/post HF4 fee treatment. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
fa1c127e12
commit
cf20259e96
2 changed files with 143 additions and 0 deletions
78
consensus/reward.go
Normal file
78
consensus/reward.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||
//
|
||||
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package consensus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
)
|
||||
|
||||
// BaseReward returns the base block reward at the given height.
|
||||
// Height 0 (genesis) returns the premine amount. All other heights
|
||||
// return the fixed block reward (1 LTHN).
|
||||
func BaseReward(height uint64) uint64 {
|
||||
if height == 0 {
|
||||
return config.Premine
|
||||
}
|
||||
return config.BlockReward
|
||||
}
|
||||
|
||||
// BlockReward applies the size penalty to a base reward. If the block
|
||||
// is within the granted full reward zone, the full base reward is returned.
|
||||
// If the block exceeds 2*medianSize, an error is returned.
|
||||
//
|
||||
// The penalty formula matches the C++ get_block_reward():
|
||||
//
|
||||
// reward = baseReward * (2*median - size) * size / median²
|
||||
//
|
||||
// Uses math/bits.Mul64 for 128-bit intermediate products to avoid overflow.
|
||||
func BlockReward(baseReward, blockSize, medianSize uint64) (uint64, error) {
|
||||
effectiveMedian := medianSize
|
||||
if effectiveMedian < config.BlockGrantedFullRewardZone {
|
||||
effectiveMedian = config.BlockGrantedFullRewardZone
|
||||
}
|
||||
|
||||
if blockSize <= effectiveMedian {
|
||||
return baseReward, nil
|
||||
}
|
||||
|
||||
if blockSize > 2*effectiveMedian {
|
||||
return 0, fmt.Errorf("consensus: block size %d too large for median %d", blockSize, effectiveMedian)
|
||||
}
|
||||
|
||||
// penalty = baseReward * (2*median - size) * size / median²
|
||||
// Use 128-bit multiplication to avoid overflow.
|
||||
twoMedian := 2 * effectiveMedian
|
||||
factor := twoMedian - blockSize // (2*median - size)
|
||||
|
||||
// hi1, lo1 = factor * blockSize
|
||||
hi1, lo1 := bits.Mul64(factor, blockSize)
|
||||
|
||||
// Since hi1 should be 0 for reasonable block sizes, simplify:
|
||||
if hi1 > 0 {
|
||||
return 0, fmt.Errorf("consensus: reward overflow")
|
||||
}
|
||||
hi2, lo2 := bits.Mul64(baseReward, lo1)
|
||||
|
||||
// Divide 128-bit result by median².
|
||||
medianSq_hi, medianSq_lo := bits.Mul64(effectiveMedian, effectiveMedian)
|
||||
_ = medianSq_hi // median² fits in 64 bits for any reasonable median
|
||||
|
||||
reward, _ := bits.Div64(hi2, lo2, medianSq_lo)
|
||||
return reward, nil
|
||||
}
|
||||
|
||||
// MinerReward calculates the total miner payout. Pre-HF4, transaction
|
||||
// fees are added to the base reward. Post-HF4 (postHF4=true), fees are
|
||||
// burned and the miner receives only the base reward.
|
||||
func MinerReward(baseReward, totalFees uint64, postHF4 bool) uint64 {
|
||||
if postHF4 {
|
||||
return baseReward
|
||||
}
|
||||
return baseReward + totalFees
|
||||
}
|
||||
65
consensus/reward_test.go
Normal file
65
consensus/reward_test.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
//go:build !integration
|
||||
|
||||
package consensus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go-blockchain/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBaseReward_Good(t *testing.T) {
|
||||
assert.Equal(t, config.Premine, BaseReward(0), "genesis returns premine")
|
||||
assert.Equal(t, config.BlockReward, BaseReward(1), "block 1 returns standard reward")
|
||||
assert.Equal(t, config.BlockReward, BaseReward(10000), "arbitrary height")
|
||||
}
|
||||
|
||||
func TestBlockReward_Good(t *testing.T) {
|
||||
base := config.BlockReward
|
||||
|
||||
// Small block: full reward.
|
||||
reward, err := BlockReward(base, 1000, config.BlockGrantedFullRewardZone)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, base, reward)
|
||||
|
||||
// Block at exactly the zone boundary: full reward.
|
||||
reward, err = BlockReward(base, config.BlockGrantedFullRewardZone, config.BlockGrantedFullRewardZone)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, base, reward)
|
||||
}
|
||||
|
||||
func TestBlockReward_Bad(t *testing.T) {
|
||||
base := config.BlockReward
|
||||
median := config.BlockGrantedFullRewardZone
|
||||
|
||||
// Block larger than 2*median: rejected.
|
||||
_, err := BlockReward(base, 2*median+1, median)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "too large")
|
||||
}
|
||||
|
||||
func TestBlockReward_Ugly(t *testing.T) {
|
||||
base := config.BlockReward
|
||||
median := config.BlockGrantedFullRewardZone
|
||||
|
||||
// Block slightly over zone: penalty applied, reward < base.
|
||||
reward, err := BlockReward(base, median+10_000, median)
|
||||
require.NoError(t, err)
|
||||
assert.Less(t, reward, base, "penalty should reduce reward")
|
||||
assert.Greater(t, reward, uint64(0), "reward should be positive")
|
||||
}
|
||||
|
||||
func TestMinerReward_Good(t *testing.T) {
|
||||
base := config.BlockReward
|
||||
fees := uint64(50_000_000_000) // 0.05 LTHN
|
||||
|
||||
// Pre-HF4: fees added.
|
||||
total := MinerReward(base, fees, false)
|
||||
assert.Equal(t, base+fees, total)
|
||||
|
||||
// Post-HF4: fees burned.
|
||||
total = MinerReward(base, fees, true)
|
||||
assert.Equal(t, base, total)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue