From a5185cacf4769a282ccb4f8803d93190cd9ca190 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 02:07:05 +0000 Subject: [PATCH] feat(mining): Config, Stats, and Miner struct TemplateProvider interface for testability. Atomic stats for lock-free reads from any goroutine. Co-Authored-By: Charon --- mining/miner.go | 110 +++++++++++++++++++++++++++++++++++++++++++ mining/miner_test.go | 41 ++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 mining/miner.go create mode 100644 mining/miner_test.go diff --git a/mining/miner.go b/mining/miner.go new file mode 100644 index 0000000..28dad2c --- /dev/null +++ b/mining/miner.go @@ -0,0 +1,110 @@ +// 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 mining + +import ( + "sync/atomic" + "time" + + "forge.lthn.ai/core/go-blockchain/rpc" + "forge.lthn.ai/core/go-blockchain/types" +) + +// TemplateProvider abstracts the RPC methods needed by the miner. +// The real rpc.Client satisfies this interface. +type TemplateProvider interface { + GetBlockTemplate(walletAddr string) (*rpc.BlockTemplateResponse, error) + SubmitBlock(hexBlob string) error + GetInfo() (*rpc.DaemonInfo, error) +} + +// Config holds the miner configuration. +type Config struct { + // DaemonURL is the JSON-RPC endpoint of the C++ daemon. + DaemonURL string + + // WalletAddr is the address that receives mining rewards. + WalletAddr string + + // PollInterval is how often to check for new blocks. Default: 3s. + PollInterval time.Duration + + // OnBlockFound is called after a solution is successfully submitted. + // May be nil. + OnBlockFound func(height uint64, hash types.Hash) + + // OnNewTemplate is called when a new block template is fetched. + // May be nil. + OnNewTemplate func(height uint64, difficulty uint64) + + // Provider is the RPC provider. If nil, a default rpc.Client is + // created from DaemonURL. + Provider TemplateProvider +} + +// Stats holds read-only mining statistics. +type Stats struct { + Hashrate float64 + BlocksFound uint64 + Height uint64 + Difficulty uint64 + Uptime time.Duration +} + +// Miner is a solo PoW miner that talks to a C++ daemon via JSON-RPC. +type Miner struct { + cfg Config + provider TemplateProvider + startTime time.Time + + // Atomic stats — accessed from Stats() on any goroutine. + hashCount atomic.Uint64 + blocksFound atomic.Uint64 + height atomic.Uint64 + difficulty atomic.Uint64 +} + +// NewMiner creates a new miner with the given configuration. +func NewMiner(cfg Config) *Miner { + if cfg.PollInterval == 0 { + cfg.PollInterval = 3 * time.Second + } + + var provider TemplateProvider + if cfg.Provider != nil { + provider = cfg.Provider + } else { + provider = rpc.NewClient(cfg.DaemonURL) + } + + return &Miner{ + cfg: cfg, + provider: provider, + } +} + +// Stats returns a snapshot of the current mining statistics. +// Safe to call from any goroutine. +func (m *Miner) Stats() Stats { + var uptime time.Duration + if !m.startTime.IsZero() { + uptime = time.Since(m.startTime) + } + + hashes := m.hashCount.Load() + var hashrate float64 + if uptime > 0 { + hashrate = float64(hashes) / uptime.Seconds() + } + + return Stats{ + Hashrate: hashrate, + BlocksFound: m.blocksFound.Load(), + Height: m.height.Load(), + Difficulty: m.difficulty.Load(), + Uptime: uptime, + } +} diff --git a/mining/miner_test.go b/mining/miner_test.go new file mode 100644 index 0000000..03f6736 --- /dev/null +++ b/mining/miner_test.go @@ -0,0 +1,41 @@ +// 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 mining + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewMiner_Good(t *testing.T) { + cfg := Config{ + DaemonURL: "http://localhost:46941", + WalletAddr: "iTHNtestaddr", + PollInterval: 5 * time.Second, + } + m := NewMiner(cfg) + + assert.NotNil(t, m) + stats := m.Stats() + assert.Equal(t, float64(0), stats.Hashrate) + assert.Equal(t, uint64(0), stats.BlocksFound) + assert.Equal(t, uint64(0), stats.Height) + assert.Equal(t, uint64(0), stats.Difficulty) + assert.Equal(t, time.Duration(0), stats.Uptime) +} + +func TestNewMiner_Good_DefaultPollInterval(t *testing.T) { + cfg := Config{ + DaemonURL: "http://localhost:46941", + WalletAddr: "iTHNtestaddr", + } + m := NewMiner(cfg) + + // PollInterval should default to 3s. + assert.Equal(t, 3*time.Second, m.cfg.PollInterval) +}