feat(mining): Config, Stats, and Miner struct
TemplateProvider interface for testability. Atomic stats for lock-free reads from any goroutine. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
c6631e555b
commit
a5185cacf4
2 changed files with 151 additions and 0 deletions
110
mining/miner.go
Normal file
110
mining/miner.go
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
41
mining/miner_test.go
Normal file
41
mining/miner_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue