90 lines
3.4 KiB
Go
90 lines
3.4 KiB
Go
package state
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// The estimate must be stable from the very first blocks of an epoch:
|
|
// three blocks on target pace should read the same ~0% as a thousand.
|
|
// The old wall-clock measurement made early readings swing wildly
|
|
// because the time since the last block, 600s on average, was counted
|
|
// into the elapsed span without a matching interval to divide it by.
|
|
func TestEstimateRetargetPercent_StableEarlyInEpoch(t *testing.T) {
|
|
// Exactly on-pace: each interval takes the consensus target.
|
|
onPace := float64(retargetInterval*targetBlockSeconds) / float64(retargetInterval-1)
|
|
for _, inEpoch := range []int64{1, 3, 10, 100, 1008, 2015} {
|
|
got := estimateRetargetPercent(inEpoch, float64(inEpoch)*onPace)
|
|
if math.Abs(got) > 0.01 {
|
|
t.Errorf("inEpoch=%d on-pace: got %+.4f%%, want ~0%%", inEpoch, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEstimateRetargetPercent_Pace(t *testing.T) {
|
|
// On-target pace per inter-block interval, including the consensus
|
|
// 2016/2015 off-by-one. Expressing the cases against it keeps the
|
|
// expected percentages exact instead of approximate.
|
|
onPace := float64(retargetInterval*targetBlockSeconds) / float64(retargetInterval-1)
|
|
tests := []struct {
|
|
name string
|
|
inEpoch int64
|
|
avgSecs float64
|
|
want float64
|
|
}{
|
|
// Hashrate 1.1x target -> difficulty rises 10% to match it.
|
|
{"fast blocks raise difficulty", 500, onPace / 1.1, 10},
|
|
// Blocks taking 1.1x as long -> difficulty falls to 1/1.1.
|
|
{"slow blocks lower difficulty", 500, onPace * 1.1, -9.09},
|
|
// Consensus clamps: 10x fast is capped at +300%.
|
|
{"clamped at +300", 100, 60, 300},
|
|
// 10x slow is capped at -75%.
|
|
{"clamped at -75", 100, 6000, -75},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := estimateRetargetPercent(tc.inEpoch, float64(tc.inEpoch)*tc.avgSecs)
|
|
if math.Abs(got-tc.want) > 0.1 {
|
|
t.Errorf("got %+.2f%%, want %+.2f%%", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Guards against a divide-by-zero or a nonsense reading at the exact
|
|
// epoch boundary, where no interval has elapsed yet.
|
|
func TestEstimateRetargetPercent_Degenerate(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
inEpoch int64
|
|
elapsed float64
|
|
}{{0, 0}, {0, 1000}, {5, 0}, {-1, 600}, {5, -600}} {
|
|
if got := estimateRetargetPercent(tc.inEpoch, tc.elapsed); got != 0 {
|
|
t.Errorf("inEpoch=%d elapsed=%v: got %v, want 0", tc.inEpoch, tc.elapsed, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The bias the fix removes: measuring to wall-clock adds the partial
|
|
// interval since the last block to the elapsed span. This reproduces
|
|
// that arithmetic to show how large the resulting error was early in an
|
|
// epoch versus at the midpoint.
|
|
func TestEstimateRetargetPercent_WallClockBiasWasWorstEarly(t *testing.T) {
|
|
onPace := float64(retargetInterval*targetBlockSeconds) / float64(retargetInterval-1)
|
|
const sinceLastBlock = 600.0 // the long-run average wait
|
|
|
|
early := estimateRetargetPercent(3, 3*onPace+sinceLastBlock)
|
|
mid := estimateRetargetPercent(1008, 1008*onPace+sinceLastBlock)
|
|
|
|
if math.Abs(early) < 15 {
|
|
t.Errorf("early-epoch wall-clock error = %+.2f%%, expected a large skew", early)
|
|
}
|
|
if math.Abs(mid) > 0.5 {
|
|
t.Errorf("mid-epoch wall-clock error = %+.2f%%, expected it to have washed out", mid)
|
|
}
|
|
// And with block timestamps, that same on-pace epoch reads ~0 at
|
|
// both points, which is the actual fix.
|
|
if got := estimateRetargetPercent(3, 3*onPace); math.Abs(got) > 0.01 {
|
|
t.Errorf("block-timestamp measurement at 3 blocks = %+.4f%%, want ~0%%", got)
|
|
}
|
|
}
|