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:
Claude 2026-02-21 22:03:28 +00:00
parent c7c536449b
commit d456b8be9b
No known key found for this signature in database
GPG key ID: AF404715446AEB41
3 changed files with 133 additions and 0 deletions

48
chain/difficulty.go Normal file
View 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
View 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)
}

View file

@ -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 {