feat: HardforkMonitor — watches chain and fires on HF activation
NewHardforkMonitor(chain, forks) with: - OnActivation callback fires when new HF height reached - RemainingBlocks() returns countdown to next HF - Start(ctx) runs background monitor with 30s poll - Context cancellation for clean shutdown Tests: creation, remaining blocks, cancellation. When HF5 activates, the Go daemon detects it automatically. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
0c4c619170
commit
b699d19ab2
2 changed files with 144 additions and 0 deletions
82
hf_monitor.go
Normal file
82
hf_monitor.go
Normal file
|
|
@ -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
|
||||
}
|
||||
62
hf_monitor_test.go
Normal file
62
hf_monitor_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue