diff --git a/consensus/reward.go b/consensus/reward.go new file mode 100644 index 0000000..f7dd0ea --- /dev/null +++ b/consensus/reward.go @@ -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 +} diff --git a/consensus/reward_test.go b/consensus/reward_test.go new file mode 100644 index 0000000..f1e8044 --- /dev/null +++ b/consensus/reward_test.go @@ -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) +}