go-blockchain/mining/miner.go

238 lines
6 KiB
Go
Raw Permalink Normal View History

// 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 (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"strconv"
"sync/atomic"
"time"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/consensus"
"dappco.re/go/core/blockchain/crypto"
"dappco.re/go/core/blockchain/rpc"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wire"
)
// 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,
}
}
// Start runs the mining loop. It blocks until ctx is cancelled.
// Returns the context error (typically context.Canceled or context.DeadlineExceeded).
func (m *Miner) Start(ctx context.Context) error {
m.startTime = time.Now()
for {
// Fetch a block template.
tmpl, err := m.provider.GetBlockTemplate(m.cfg.WalletAddr)
if err != nil {
// Transient RPC error — wait and retry.
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(m.cfg.PollInterval):
continue
}
}
// Parse difficulty.
diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64)
if err != nil {
return coreerr.E("Miner.Start", fmt.Sprintf("mining: invalid difficulty %q", tmpl.Difficulty), err)
}
// Decode the block template blob.
blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob)
if err != nil {
return coreerr.E("Miner.Start", "mining: invalid template blob hex", err)
}
dec := wire.NewDecoder(bytes.NewReader(blobBytes))
block := wire.DecodeBlock(dec)
if dec.Err() != nil {
return coreerr.E("Miner.Start", "mining: decode template", dec.Err())
}
// Update stats.
m.height.Store(tmpl.Height)
m.difficulty.Store(diff)
if m.cfg.OnNewTemplate != nil {
m.cfg.OnNewTemplate(tmpl.Height, diff)
}
// Compute the header mining hash (once per template).
headerHash := HeaderMiningHash(&block)
// Mine until solution found or template becomes stale.
if err := m.mine(ctx, &block, headerHash, diff); err != nil {
return err
}
}
}
// mine grinds nonces against the given header hash and difficulty.
// Returns nil when a new template should be fetched (new block detected).
// Returns ctx.Err() when shutdown is requested.
func (m *Miner) mine(ctx context.Context, block *types.Block, headerHash [32]byte, difficulty uint64) error {
pollTicker := time.NewTicker(m.cfg.PollInterval)
defer pollTicker.Stop()
currentHeight := m.height.Load()
for nonce := uint64(0); ; nonce++ {
// Check for shutdown or poll trigger.
select {
case <-ctx.Done():
return ctx.Err()
case <-pollTicker.C:
// Check if chain has advanced.
info, err := m.provider.GetInfo()
if err == nil && info.Height > currentHeight {
return nil // fetch new template
}
continue
default:
}
// Compute RandomX hash.
var input [40]byte
copy(input[:32], headerHash[:])
binary.LittleEndian.PutUint64(input[32:], nonce)
powHash, err := crypto.RandomXHash(RandomXKey, input[:])
if err != nil {
return coreerr.E("Miner.mine", "mining: RandomX hash", err)
}
m.hashCount.Add(1)
if consensus.CheckDifficulty(types.Hash(powHash), difficulty) {
// Solution found!
block.Nonce = nonce
var buf bytes.Buffer
enc := wire.NewEncoder(&buf)
wire.EncodeBlock(enc, block)
if enc.Err() != nil {
return coreerr.E("Miner.mine", "mining: encode solution", enc.Err())
}
hexBlob := hex.EncodeToString(buf.Bytes())
if err := m.provider.SubmitBlock(hexBlob); err != nil {
return coreerr.E("Miner.mine", "mining: submit block", err)
}
m.blocksFound.Add(1)
if m.cfg.OnBlockFound != nil {
m.cfg.OnBlockFound(currentHeight, wire.BlockHash(block))
}
return nil // fetch new template
}
}
}