diff --git a/block.go b/block.go index fc9ed27..c73706d 100644 --- a/block.go +++ b/block.go @@ -1399,6 +1399,7 @@ func (c *Chain) RecentBlockStats(samples int) (hashrate float64, avgBlockTime fl return 0, 0 } var totalTime int64 + var totalWork uint64 var count int for h := height; h > 0 && count < samples; h-- { block := c.GetBlockByHeight(h) @@ -1409,6 +1410,7 @@ func (c *Chain) RecentBlockStats(samples int) (hashrate float64, avgBlockTime fl dt := block.Header.Timestamp - prev.Header.Timestamp if dt > 0 { totalTime += dt + totalWork += block.Header.Difficulty count++ } } @@ -1416,7 +1418,10 @@ func (c *Chain) RecentBlockStats(samples int) (hashrate float64, avgBlockTime fl return 0, 0 } avgBlockTime = float64(totalTime) / float64(count) - hashrate = float64(c.NextDifficulty()) / avgBlockTime + // Each sampled interval ended with `block`, so its observed work is that + // block's difficulty. Using the next-block difficulty here biases the + // estimate whenever difficulty changes during the sample window. + hashrate = float64(totalWork) / float64(totalTime) return hashrate, avgBlockTime } diff --git a/block_stats_test.go b/block_stats_test.go new file mode 100644 index 0000000..fbb0a48 --- /dev/null +++ b/block_stats_test.go @@ -0,0 +1,48 @@ +package main + +import ( + "math" + "testing" +) + +func TestRecentBlockStatsUsesObservedIntervalWork(t *testing.T) { + chain, storage, cleanup := mustCreateTestChain(t) + defer cleanup() + mustAddGenesisBlock(t, chain) + + genesis := chain.GetBlockByHeight(0) + prev := genesis + timestamp := genesis.Header.Timestamp + var totalWork uint64 + for height, difficulty := range []uint64{100, 300, 500} { + timestamp += int64((height + 1) * 10) + block := &Block{Header: BlockHeader{ + Version: 1, + Height: uint64(height + 1), + PrevHash: prev.Hash(), + Timestamp: timestamp, + Difficulty: difficulty, + }} + if err := storage.SaveBlock(block); err != nil { + t.Fatalf("save block %d: %v", height+1, err) + } + if err := storage.SetMainChainBlock(uint64(height+1), block.Hash()); err != nil { + t.Fatalf("index block %d: %v", height+1, err) + } + prev = block + totalWork += difficulty + } + + chain.mu.Lock() + chain.height = 3 + chain.bestHash = prev.Hash() + chain.mu.Unlock() + + hashrate, avgBlockTime := chain.RecentBlockStats(3) + if want := 20.0; math.Abs(avgBlockTime-want) > 1e-9 { + t.Fatalf("average block time = %v, want %v", avgBlockTime, want) + } + if want := float64(totalWork) / 60.0; math.Abs(hashrate-want) > 1e-9 { + t.Fatalf("network hashrate = %v, want observed work/time %v", hashrate, want) + } +}