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>
72 lines
2.1 KiB
Go
72 lines
2.1 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 chain
|
|
|
|
import (
|
|
"math/big"
|
|
|
|
"forge.lthn.ai/core/go-blockchain/config"
|
|
"forge.lthn.ai/core/go-blockchain/difficulty"
|
|
)
|
|
|
|
// NextDifficulty computes the expected difficulty for the block at the given
|
|
// height, using the LWMA algorithm over stored block history.
|
|
//
|
|
// The genesis block (height 0) is excluded from the difficulty window,
|
|
// matching the C++ daemon's load_targetdata_cache which skips index 0.
|
|
//
|
|
// The target block time depends on the hardfork schedule: 120s pre-HF2,
|
|
// 240s post-HF2 (matching DIFFICULTY_POW_TARGET_HF6 in the C++ source).
|
|
func (c *Chain) NextDifficulty(height uint64, forks []config.HardFork) (uint64, error) {
|
|
if height == 0 {
|
|
return 1, nil
|
|
}
|
|
|
|
// LWMA needs N+1 entries (N solve-time intervals).
|
|
// Start from height 1 — genesis is excluded from the difficulty window.
|
|
maxLookback := difficulty.LWMAWindow + 1
|
|
lookback := height // height excludes genesis since we start from 1
|
|
if lookback > maxLookback {
|
|
lookback = maxLookback
|
|
}
|
|
|
|
// Start from max(1, height - lookback) to exclude genesis.
|
|
startHeight := height - lookback
|
|
if startHeight == 0 {
|
|
startHeight = 1
|
|
lookback = height - 1
|
|
}
|
|
|
|
if lookback == 0 {
|
|
return 1, nil
|
|
}
|
|
|
|
count := int(lookback)
|
|
timestamps := make([]uint64, count)
|
|
cumulDiffs := make([]*big.Int, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
meta, err := c.getBlockMeta(startHeight + uint64(i))
|
|
if err != nil {
|
|
// Fewer blocks than expected — use what we have.
|
|
timestamps = timestamps[:i]
|
|
cumulDiffs = cumulDiffs[:i]
|
|
break
|
|
}
|
|
timestamps[i] = meta.Timestamp
|
|
cumulDiffs[i] = new(big.Int).SetUint64(meta.CumulativeDiff)
|
|
}
|
|
|
|
// Determine the target block time based on hardfork status.
|
|
// HF2 doubles the target from 120s to 240s.
|
|
target := config.DifficultyPowTarget
|
|
if config.IsHardForkActive(forks, config.HF2, height) {
|
|
target = config.DifficultyPowTargetHF6
|
|
}
|
|
|
|
result := difficulty.NextDifficulty(timestamps, cumulDiffs, target)
|
|
return result.Uint64(), nil
|
|
}
|