feat(chain): add NextDifficulty for local LWMA computation
Reads stored block timestamps and cumulative difficulties, calls difficulty.NextDifficulty() with config.BlockTarget. Returns uint64. Adds getBlockMeta() for lightweight metadata-only lookups. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
c7c536449b
commit
d456b8be9b
3 changed files with 133 additions and 0 deletions
48
chain/difficulty.go
Normal file
48
chain/difficulty.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// 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.
|
||||
func (c *Chain) NextDifficulty(height uint64) (uint64, error) {
|
||||
if height == 0 {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// Determine how far back to look.
|
||||
lookback := height
|
||||
if lookback > difficulty.BlocksCount {
|
||||
lookback = difficulty.BlocksCount
|
||||
}
|
||||
|
||||
startHeight := height - lookback
|
||||
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)
|
||||
}
|
||||
|
||||
result := difficulty.NextDifficulty(timestamps, cumulDiffs, config.BlockTarget)
|
||||
return result.Uint64(), nil
|
||||
}
|
||||
71
chain/difficulty_test.go
Normal file
71
chain/difficulty_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
store "forge.lthn.ai/core/go-store"
|
||||
"forge.lthn.ai/core/go-blockchain/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNextDifficulty_Genesis(t *testing.T) {
|
||||
s, err := store.New(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
c := New(s)
|
||||
diff, err := c.NextDifficulty(0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), diff)
|
||||
}
|
||||
|
||||
func TestNextDifficulty_FewBlocks(t *testing.T) {
|
||||
s, err := store.New(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
c := New(s)
|
||||
|
||||
// Store 5 blocks with constant 120s intervals and difficulty 1000.
|
||||
baseDiff := uint64(1000)
|
||||
for i := uint64(0); i < 5; i++ {
|
||||
err := c.PutBlock(&types.Block{}, &BlockMeta{
|
||||
Hash: types.Hash{byte(i + 1)},
|
||||
Height: i,
|
||||
Timestamp: i * 120,
|
||||
Difficulty: baseDiff,
|
||||
CumulativeDiff: baseDiff * (i + 1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Next difficulty for height 5 should be approximately 1000.
|
||||
diff, err := c.NextDifficulty(5)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, diff, uint64(0))
|
||||
|
||||
// With constant intervals at target, difficulty should be close to base.
|
||||
// Allow 10% tolerance.
|
||||
low := baseDiff - baseDiff/10
|
||||
high := baseDiff + baseDiff/10
|
||||
require.GreaterOrEqual(t, diff, low, "difficulty %d below expected range [%d, %d]", diff, low, high)
|
||||
require.LessOrEqual(t, diff, high, "difficulty %d above expected range [%d, %d]", diff, low, high)
|
||||
}
|
||||
|
||||
func TestNextDifficulty_EmptyChain(t *testing.T) {
|
||||
s, err := store.New(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
c := New(s)
|
||||
|
||||
// Height 1 with no blocks stored — should return starter difficulty.
|
||||
diff, err := c.NextDifficulty(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), diff)
|
||||
}
|
||||
|
|
@ -159,6 +159,20 @@ func (c *Chain) HasTransaction(hash types.Hash) bool {
|
|||
return err == nil
|
||||
}
|
||||
|
||||
// getBlockMeta retrieves only the metadata for a block at the given height,
|
||||
// without decoding the wire blob. Useful for lightweight lookups.
|
||||
func (c *Chain) getBlockMeta(height uint64) (*BlockMeta, error) {
|
||||
val, err := c.store.Get(groupBlocks, heightKey(height))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("chain: block meta %d: %w", height, err)
|
||||
}
|
||||
var rec blockRecord
|
||||
if err := json.Unmarshal([]byte(val), &rec); err != nil {
|
||||
return nil, fmt.Errorf("chain: unmarshal block meta %d: %w", height, err)
|
||||
}
|
||||
return &rec.Meta, nil
|
||||
}
|
||||
|
||||
func decodeBlockRecord(val string) (*types.Block, *BlockMeta, error) {
|
||||
var rec blockRecord
|
||||
if err := json.Unmarshal([]byte(val), &rec); err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue