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:
Claude 2026-02-21 00:42:46 +00:00
parent fa1c127e12
commit cf20259e96
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 143 additions and 0 deletions

78
consensus/reward.go Normal file
View 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
View 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)
}