2026-02-20 15:10:33 +00:00
|
|
|
// 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"
|
2026-02-21 21:59:41 +00:00
|
|
|
|
|
|
|
|
"forge.lthn.ai/core/go-blockchain/config"
|
2026-02-20 15:10:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestNextDifficulty_Good(t *testing.T) {
|
|
|
|
|
// Synthetic test: constant block times at exactly the target interval.
|
2026-02-21 22:32:57 +00:00
|
|
|
// With the LWMA-1 formula, constant D gives next_D = D/n for full window.
|
2026-02-21 21:59:41 +00:00
|
|
|
target := config.BlockTarget
|
2026-02-20 15:10:33 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 22:32:57 +00:00
|
|
|
// 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)
|
2026-02-20 15:10:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNextDifficultyEmpty_Good(t *testing.T) {
|
|
|
|
|
// Empty input should return starter difficulty.
|
2026-02-21 21:59:41 +00:00
|
|
|
result := NextDifficulty(nil, nil, config.BlockTarget)
|
2026-02-20 15:10:33 +00:00
|
|
|
if result.Cmp(StarterDifficulty) != 0 {
|
2026-02-21 21:59:41 +00:00
|
|
|
t.Errorf("NextDifficulty(nil, nil, %d) = %s, want %s", config.BlockTarget, result, StarterDifficulty)
|
2026-02-20 15:10:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNextDifficultySingleEntry_Good(t *testing.T) {
|
|
|
|
|
// A single entry is insufficient for calculation.
|
|
|
|
|
timestamps := []uint64{1000}
|
|
|
|
|
diffs := []*big.Int{big.NewInt(100)}
|
2026-02-21 21:59:41 +00:00
|
|
|
result := NextDifficulty(timestamps, diffs, config.BlockTarget)
|
2026-02-20 15:10:33 +00:00
|
|
|
if result.Cmp(StarterDifficulty) != 0 {
|
|
|
|
|
t.Errorf("NextDifficulty with single entry = %s, want %s", result, StarterDifficulty)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNextDifficultyFastBlocks_Good(t *testing.T) {
|
2026-02-21 22:32:57 +00:00
|
|
|
// When blocks come faster than the target, difficulty should increase
|
|
|
|
|
// relative to the constant-rate result.
|
2026-02-21 21:59:41 +00:00
|
|
|
target := config.BlockTarget
|
2026-02-20 15:10:33 +00:00
|
|
|
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)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 22:32:57 +00:00
|
|
|
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)
|
2026-02-20 15:10:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNextDifficultySlowBlocks_Good(t *testing.T) {
|
2026-02-21 22:32:57 +00:00
|
|
|
// When blocks come slower than the target, difficulty should decrease
|
|
|
|
|
// relative to the constant-rate result.
|
2026-02-21 21:59:41 +00:00
|
|
|
target := config.BlockTarget
|
2026-02-20 15:10:33 +00:00
|
|
|
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)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 22:32:57 +00:00
|
|
|
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)
|
2026-02-20 15:10:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)}
|
2026-02-21 21:59:41 +00:00
|
|
|
result := NextDifficulty(timestamps, diffs, config.BlockTarget)
|
2026-02-20 15:10:33 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2026-02-21 22:32:57 +00:00
|
|
|
if LWMAWindow != 60 {
|
|
|
|
|
t.Errorf("LWMAWindow: got %d, want 60", LWMAWindow)
|
|
|
|
|
}
|
2026-02-20 15:10:33 +00:00
|
|
|
}
|