Rewrites the LWMA difficulty algorithm to match the C++ daemon exactly: - Uses N=60 window with linear weighting (position 1..n) - Clamps solve times to [-6T, 6T] - Excludes genesis block from the difficulty window - Selects target based on hardfork: 120s pre-HF2, 240s post-HF2 On testnet, HF2 activates at height 10 (active from height 11), doubling the target from 120s to 240s. The previous fixed 120s target produced exactly half the expected difficulty from height 11 onward. Integration test verifies all 2576 testnet blocks match the daemon. Co-Authored-By: Charon <charon@lethean.io>
149 lines
4.7 KiB
Go
149 lines
4.7 KiB
Go
// 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 difficulty
|
|
|
|
import (
|
|
"math/big"
|
|
"testing"
|
|
|
|
"forge.lthn.ai/core/go-blockchain/config"
|
|
)
|
|
|
|
func TestNextDifficulty_Good(t *testing.T) {
|
|
// Synthetic test: constant block times at exactly the target interval.
|
|
// With the LWMA-1 formula, constant D gives next_D = D/n for full window.
|
|
target := config.BlockTarget
|
|
const numBlocks = 100
|
|
|
|
timestamps := make([]uint64, numBlocks)
|
|
cumulativeDiffs := make([]*big.Int, numBlocks)
|
|
|
|
baseDifficulty := big.NewInt(1000)
|
|
for i := 0; i < numBlocks; i++ {
|
|
timestamps[i] = uint64(i) * target
|
|
cumulativeDiffs[i] = new(big.Int).Mul(baseDifficulty, big.NewInt(int64(i)))
|
|
}
|
|
|
|
result := NextDifficulty(timestamps, cumulativeDiffs, target)
|
|
if result.Sign() <= 0 {
|
|
t.Fatalf("NextDifficulty returned non-positive value: %s", result)
|
|
}
|
|
|
|
// LWMA trims to last 61 entries (N+1=61), giving n=60 intervals.
|
|
// Formula: D/n = 1000/60 = 16.
|
|
expected := big.NewInt(16)
|
|
if result.Cmp(expected) != 0 {
|
|
t.Errorf("NextDifficulty with constant intervals: got %s, expected %s", result, expected)
|
|
}
|
|
}
|
|
|
|
func TestNextDifficultyEmpty_Good(t *testing.T) {
|
|
// Empty input should return starter difficulty.
|
|
result := NextDifficulty(nil, nil, config.BlockTarget)
|
|
if result.Cmp(StarterDifficulty) != 0 {
|
|
t.Errorf("NextDifficulty(nil, nil, %d) = %s, want %s", config.BlockTarget, result, StarterDifficulty)
|
|
}
|
|
}
|
|
|
|
func TestNextDifficultySingleEntry_Good(t *testing.T) {
|
|
// A single entry is insufficient for calculation.
|
|
timestamps := []uint64{1000}
|
|
diffs := []*big.Int{big.NewInt(100)}
|
|
result := NextDifficulty(timestamps, diffs, config.BlockTarget)
|
|
if result.Cmp(StarterDifficulty) != 0 {
|
|
t.Errorf("NextDifficulty with single entry = %s, want %s", result, StarterDifficulty)
|
|
}
|
|
}
|
|
|
|
func TestNextDifficultyFastBlocks_Good(t *testing.T) {
|
|
// When blocks come faster than the target, difficulty should increase
|
|
// relative to the constant-rate result.
|
|
target := config.BlockTarget
|
|
const numBlocks = 50
|
|
const actualInterval uint64 = 60 // half the target — blocks are too fast
|
|
|
|
timestamps := make([]uint64, numBlocks)
|
|
cumulativeDiffs := make([]*big.Int, numBlocks)
|
|
|
|
baseDifficulty := big.NewInt(1000)
|
|
for i := 0; i < numBlocks; i++ {
|
|
timestamps[i] = uint64(i) * actualInterval
|
|
cumulativeDiffs[i] = new(big.Int).Mul(baseDifficulty, big.NewInt(int64(i)))
|
|
}
|
|
|
|
resultFast := NextDifficulty(timestamps, cumulativeDiffs, target)
|
|
|
|
// Now compute with on-target intervals for comparison.
|
|
timestampsTarget := make([]uint64, numBlocks)
|
|
for i := 0; i < numBlocks; i++ {
|
|
timestampsTarget[i] = uint64(i) * target
|
|
}
|
|
resultTarget := NextDifficulty(timestampsTarget, cumulativeDiffs, target)
|
|
|
|
if resultFast.Cmp(resultTarget) <= 0 {
|
|
t.Errorf("fast blocks (%s) should produce higher difficulty than target-rate blocks (%s)",
|
|
resultFast, resultTarget)
|
|
}
|
|
}
|
|
|
|
func TestNextDifficultySlowBlocks_Good(t *testing.T) {
|
|
// When blocks come slower than the target, difficulty should decrease
|
|
// relative to the constant-rate result.
|
|
target := config.BlockTarget
|
|
const numBlocks = 50
|
|
const actualInterval uint64 = 240 // double the target — blocks are too slow
|
|
|
|
timestamps := make([]uint64, numBlocks)
|
|
cumulativeDiffs := make([]*big.Int, numBlocks)
|
|
|
|
baseDifficulty := big.NewInt(1000)
|
|
for i := 0; i < numBlocks; i++ {
|
|
timestamps[i] = uint64(i) * actualInterval
|
|
cumulativeDiffs[i] = new(big.Int).Mul(baseDifficulty, big.NewInt(int64(i)))
|
|
}
|
|
|
|
resultSlow := NextDifficulty(timestamps, cumulativeDiffs, target)
|
|
|
|
// Compute with on-target intervals for comparison.
|
|
timestampsTarget := make([]uint64, numBlocks)
|
|
for i := 0; i < numBlocks; i++ {
|
|
timestampsTarget[i] = uint64(i) * target
|
|
}
|
|
resultTarget := NextDifficulty(timestampsTarget, cumulativeDiffs, target)
|
|
|
|
if resultSlow.Cmp(resultTarget) >= 0 {
|
|
t.Errorf("slow blocks (%s) should produce lower difficulty than target-rate blocks (%s)",
|
|
resultSlow, resultTarget)
|
|
}
|
|
}
|
|
|
|
func TestNextDifficulty_Ugly(t *testing.T) {
|
|
// Two entries with zero time span — should handle gracefully.
|
|
timestamps := []uint64{1000, 1000}
|
|
diffs := []*big.Int{big.NewInt(0), big.NewInt(100)}
|
|
result := NextDifficulty(timestamps, diffs, config.BlockTarget)
|
|
if result.Sign() <= 0 {
|
|
t.Errorf("NextDifficulty with zero time span should still return positive, got %s", result)
|
|
}
|
|
}
|
|
|
|
func TestConstants_Good(t *testing.T) {
|
|
if Window != 720 {
|
|
t.Errorf("Window: got %d, want 720", Window)
|
|
}
|
|
if Lag != 15 {
|
|
t.Errorf("Lag: got %d, want 15", Lag)
|
|
}
|
|
if Cut != 60 {
|
|
t.Errorf("Cut: got %d, want 60", Cut)
|
|
}
|
|
if BlocksCount != 735 {
|
|
t.Errorf("BlocksCount: got %d, want 735", BlocksCount)
|
|
}
|
|
if LWMAWindow != 60 {
|
|
t.Errorf("LWMAWindow: got %d, want 60", LWMAWindow)
|
|
}
|
|
}
|