portal/renterd/internal/node/miner.go

151 lines
3.9 KiB
Go

// TODO: remove this file when we can import it from hostd
package node
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"sync"
"go.sia.tech/core/types"
"go.sia.tech/siad/crypto"
"go.sia.tech/siad/modules"
stypes "go.sia.tech/siad/types"
"lukechampine.com/frand"
)
const solveAttempts = 1e4
type (
// Consensus defines a minimal interface needed by the miner to interact
// with the consensus set
Consensus interface {
AcceptBlock(context.Context, types.Block) error
}
// A Miner is a CPU miner that can mine blocks, sending the reward to a
// specified address.
Miner struct {
consensus Consensus
mu sync.Mutex
height stypes.BlockHeight
target stypes.Target
currentBlockID stypes.BlockID
txnsets map[modules.TransactionSetID][]stypes.TransactionID
transactions []stypes.Transaction
}
)
var errFailedToSolve = errors.New("failed to solve block")
// ProcessConsensusChange implements modules.ConsensusSetSubscriber.
func (m *Miner) ProcessConsensusChange(cc modules.ConsensusChange) {
m.mu.Lock()
defer m.mu.Unlock()
m.target = cc.ChildTarget
m.currentBlockID = cc.AppliedBlocks[len(cc.AppliedBlocks)-1].ID()
m.height = cc.BlockHeight
}
// ReceiveUpdatedUnconfirmedTransactions implements modules.TransactionPoolSubscriber
func (m *Miner) ReceiveUpdatedUnconfirmedTransactions(diff *modules.TransactionPoolDiff) {
m.mu.Lock()
defer m.mu.Unlock()
reverted := make(map[stypes.TransactionID]bool)
for _, setID := range diff.RevertedTransactions {
for _, txnID := range m.txnsets[setID] {
reverted[txnID] = true
}
}
filtered := m.transactions[:0]
for _, txn := range m.transactions {
if reverted[txn.ID()] {
continue
}
filtered = append(filtered, txn)
}
for _, txnset := range diff.AppliedTransactions {
m.txnsets[txnset.ID] = txnset.IDs
filtered = append(filtered, txnset.Transactions...)
}
m.transactions = filtered
}
// mineBlock attempts to mine a block and add it to the consensus set.
func (m *Miner) mineBlock(addr stypes.UnlockHash) error {
m.mu.Lock()
block := stypes.Block{
ParentID: m.currentBlockID,
Timestamp: stypes.CurrentTimestamp(),
}
randBytes := frand.Bytes(stypes.SpecifierLen)
randTxn := stypes.Transaction{
ArbitraryData: [][]byte{append(modules.PrefixNonSia[:], randBytes...)},
}
block.Transactions = append([]stypes.Transaction{randTxn}, m.transactions...)
block.MinerPayouts = append(block.MinerPayouts, stypes.SiacoinOutput{
Value: block.CalculateSubsidy(m.height + 1),
UnlockHash: addr,
})
target := m.target
m.mu.Unlock()
merkleRoot := block.MerkleRoot()
header := make([]byte, 80)
copy(header, block.ParentID[:])
binary.LittleEndian.PutUint64(header[40:48], uint64(block.Timestamp))
copy(header[48:], merkleRoot[:])
var nonce uint64
var solved bool
for i := 0; i < solveAttempts; i++ {
id := crypto.HashBytes(header)
if bytes.Compare(target[:], id[:]) >= 0 {
block.Nonce = *(*stypes.BlockNonce)(header[32:40])
solved = true
break
}
binary.LittleEndian.PutUint64(header[32:], nonce)
nonce += stypes.ASICHardforkFactor
}
if !solved {
return errFailedToSolve
}
var b types.Block
convertToCore(&block, &b)
if err := m.consensus.AcceptBlock(context.Background(), b); err != nil {
return fmt.Errorf("failed to get block accepted: %w", err)
}
return nil
}
// Mine mines n blocks, sending the reward to addr
func (m *Miner) Mine(addr types.Address, n int) error {
var err error
for mined := 1; mined <= n; {
// return the error only if the miner failed to solve the block,
// ignore any consensus related errors
if err = m.mineBlock(stypes.UnlockHash(addr)); errors.Is(err, errFailedToSolve) {
return fmt.Errorf("failed to mine block %v: %w", mined, errFailedToSolve)
}
mined++
}
return nil
}
// NewMiner initializes a new CPU miner
func NewMiner(consensus Consensus) *Miner {
return &Miner{
consensus: consensus,
txnsets: make(map[modules.TransactionSetID][]stypes.TransactionID),
}
}