diff --git a/chain/difficulty.go b/chain/difficulty.go new file mode 100644 index 0000000..71ce872 --- /dev/null +++ b/chain/difficulty.go @@ -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 +} diff --git a/chain/difficulty_test.go b/chain/difficulty_test.go new file mode 100644 index 0000000..c93e99f --- /dev/null +++ b/chain/difficulty_test.go @@ -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) +} diff --git a/chain/store.go b/chain/store.go index fc43219..12bd920 100644 --- a/chain/store.go +++ b/chain/store.go @@ -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 {