diff --git a/hf_monitor.go b/hf_monitor.go new file mode 100644 index 0000000..642b9e9 --- /dev/null +++ b/hf_monitor.go @@ -0,0 +1,82 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// SPDX-License-Identifier: EUPL-1.2 + +package blockchain + +import ( + "context" + "time" + + "dappco.re/go/core" + + "dappco.re/go/core/blockchain/chain" + "dappco.re/go/core/blockchain/config" +) + +// HardforkMonitor watches for hardfork activations and fires callbacks. +// +// monitor := blockchain.NewHardforkMonitor(chain, config.TestnetForks) +// monitor.OnActivation = func(version int, height uint64) { ... } +// monitor.Start(ctx) +type HardforkMonitor struct { + chain *chain.Chain + forks []config.HardFork + OnActivation func(version int, height uint64) + activated map[int]bool +} + +// NewHardforkMonitor creates a monitor that watches for HF activations. +// +// monitor := blockchain.NewHardforkMonitor(ch, config.TestnetForks) +func NewHardforkMonitor(ch *chain.Chain, forks []config.HardFork) *HardforkMonitor { + return &HardforkMonitor{ + chain: ch, + forks: forks, + activated: make(map[int]bool), + } +} + +// Start begins monitoring in the background. Checks every 30 seconds. +// +// go monitor.Start(ctx) +func (m *HardforkMonitor) Start(ctx context.Context) { + // Mark already-active forks + height, _ := m.chain.Height() + for _, f := range m.forks { + if height >= f.Height { + m.activated[int(f.Version)] = true + } + } + + for { + select { + case <-ctx.Done(): + return + case <-time.After(30 * time.Second): + } + + height, _ := m.chain.Height() + for _, f := range m.forks { + if height >= f.Height && !m.activated[int(f.Version)] { + m.activated[int(f.Version)] = true + core.Print(nil, "HARDFORK %d ACTIVATED at height %d", f.Version, height) + if m.OnActivation != nil { + m.OnActivation(int(f.Version), height) + } + } + } + } +} + +// RemainingBlocks returns blocks until next inactive hardfork. +// +// blocks, version := monitor.RemainingBlocks() +func (m *HardforkMonitor) RemainingBlocks() (uint64, int) { + height, _ := m.chain.Height() + for _, f := range m.forks { + if height < f.Height { + return f.Height - height, int(f.Version) + } + } + return 0, -1 +} diff --git a/hf_monitor_test.go b/hf_monitor_test.go new file mode 100644 index 0000000..53dcaca --- /dev/null +++ b/hf_monitor_test.go @@ -0,0 +1,62 @@ +package blockchain + +import ( + "context" + "testing" + "time" + + "dappco.re/go/core/blockchain/chain" + "dappco.re/go/core/blockchain/config" + store "dappco.re/go/core/store" +) + +func TestHardforkMonitor_New_Good(t *testing.T) { + s, _ := store.New(t.TempDir() + "/test.db") + defer s.Close() + ch := chain.New(s) + + monitor := NewHardforkMonitor(ch, config.TestnetForks) + if monitor == nil { + t.Fatal("monitor is nil") + } +} + +func TestHardforkMonitor_RemainingBlocks_Good(t *testing.T) { + s, _ := store.New(t.TempDir() + "/test.db") + defer s.Close() + ch := chain.New(s) + + monitor := NewHardforkMonitor(ch, config.TestnetForks) + blocks, version := monitor.RemainingBlocks() + // On empty chain, first fork should have remaining blocks + if blocks == 0 && version == -1 { + // All forks at height 0 — this is valid for testnet where HF0-1 are at 0 + return + } + if version < 0 { + t.Error("expected pending hardfork") + } +} + +func TestHardforkMonitor_Start_Cancellation_Good(t *testing.T) { + s, _ := store.New(t.TempDir() + "/test.db") + defer s.Close() + ch := chain.New(s) + + monitor := NewHardforkMonitor(ch, config.TestnetForks) + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + done := make(chan struct{}) + go func() { + monitor.Start(ctx) + close(done) + }() + + select { + case <-done: + // Good — monitor stopped on context cancellation + case <-time.After(2 * time.Second): + t.Error("monitor didn't stop on context cancellation") + } +}