github.com/dominant-strategies/go-quai@v0.28.2/consensus/blake3pow/sealer.go (about)

     1  package blake3pow
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	crand "crypto/rand"
     7  	"encoding/json"
     8  	"errors"
     9  	"math"
    10  	"math/big"
    11  	"math/rand"
    12  	"net/http"
    13  	"runtime"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/dominant-strategies/go-quai/common"
    18  	"github.com/dominant-strategies/go-quai/common/hexutil"
    19  	"github.com/dominant-strategies/go-quai/core/types"
    20  	"github.com/dominant-strategies/go-quai/log"
    21  )
    22  
    23  const (
    24  	// staleThreshold is the maximum depth of the acceptable stale but valid blake3pow solution.
    25  	staleThreshold = 7
    26  	mantBits       = 64
    27  )
    28  
    29  var (
    30  	errNoMiningWork      = errors.New("no mining work available yet")
    31  	errInvalidSealResult = errors.New("invalid or stale proof-of-work solution")
    32  )
    33  
    34  // Seal implements consensus.Engine, attempting to find a nonce that satisfies
    35  // the header's difficulty requirements.
    36  func (blake3pow *Blake3pow) Seal(header *types.Header, results chan<- *types.Header, stop <-chan struct{}) error {
    37  	// If we're running a fake PoW, simply return a 0 nonce immediately
    38  	if blake3pow.config.PowMode == ModeFake || blake3pow.config.PowMode == ModeFullFake {
    39  		header.SetNonce(types.BlockNonce{})
    40  		select {
    41  		case results <- header:
    42  		default:
    43  			blake3pow.config.Log.Warn("Sealing result is not read by miner", "mode", "fake", "sealhash", header.SealHash())
    44  		}
    45  		return nil
    46  	}
    47  	// If we're running a shared PoW, delegate sealing to it
    48  	if blake3pow.shared != nil {
    49  		return blake3pow.shared.Seal(header, results, stop)
    50  	}
    51  	// Create a runner and the multiple search threads it directs
    52  	abort := make(chan struct{})
    53  
    54  	blake3pow.lock.Lock()
    55  	threads := blake3pow.threads
    56  	if blake3pow.rand == nil {
    57  		seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
    58  		if err != nil {
    59  			blake3pow.lock.Unlock()
    60  			return err
    61  		}
    62  		blake3pow.rand = rand.New(rand.NewSource(seed.Int64()))
    63  	}
    64  	blake3pow.lock.Unlock()
    65  	if threads == 0 {
    66  		threads = runtime.NumCPU()
    67  	}
    68  	if threads < 0 {
    69  		threads = 0 // Allows disabling local mining without extra logic around local/remote
    70  	}
    71  	// Push new work to remote sealer
    72  	if blake3pow.remote != nil {
    73  		blake3pow.remote.workCh <- &sealTask{header: header, results: results}
    74  	}
    75  	var (
    76  		pend   sync.WaitGroup
    77  		locals = make(chan *types.Header)
    78  	)
    79  	for i := 0; i < threads; i++ {
    80  		pend.Add(1)
    81  		go func(id int, nonce uint64) {
    82  			defer pend.Done()
    83  			blake3pow.mine(header, id, nonce, abort, locals)
    84  		}(i, uint64(blake3pow.rand.Int63()))
    85  	}
    86  	// Wait until sealing is terminated or a nonce is found
    87  	go func() {
    88  		var result *types.Header
    89  		select {
    90  		case <-stop:
    91  			// Outside abort, stop all miner threads
    92  			close(abort)
    93  		case result = <-locals:
    94  			// One of the threads found a block, abort all others
    95  			select {
    96  			case results <- result:
    97  			default:
    98  				blake3pow.config.Log.Warn("Sealing result is not read by miner", "mode", "local", "sealhash", header.SealHash())
    99  			}
   100  			close(abort)
   101  		case <-blake3pow.update:
   102  			// Thread count was changed on user request, restart
   103  			close(abort)
   104  			if err := blake3pow.Seal(header, results, stop); err != nil {
   105  				blake3pow.config.Log.Error("Failed to restart sealing after update", "err", err)
   106  			}
   107  		}
   108  		// Wait for all miners to terminate and return the block
   109  		pend.Wait()
   110  	}()
   111  	return nil
   112  }
   113  
   114  // mine is the actual proof-of-work miner that searches for a nonce starting from
   115  // seed that results in correct final header difficulty.
   116  func (blake3pow *Blake3pow) mine(header *types.Header, id int, seed uint64, abort chan struct{}, found chan *types.Header) {
   117  	// Extract some data from the header
   118  	var (
   119  		target = new(big.Int).Div(big2e256, header.Difficulty())
   120  	)
   121  	// Start generating random nonces until we abort or find a good one
   122  	var (
   123  		attempts  = int64(0)
   124  		nonce     = seed
   125  		powBuffer = new(big.Int)
   126  	)
   127  	logger := log.Log
   128  	logger.Trace("Started blake3pow search for new nonces", "seed", seed)
   129  search:
   130  	for {
   131  		select {
   132  		case <-abort:
   133  			// Mining terminated, update stats and abort
   134  			logger.Trace("Blake3pow nonce search aborted", "attempts", nonce-seed)
   135  			blake3pow.hashrate.Mark(attempts)
   136  			break search
   137  
   138  		default:
   139  			// We don't have to update hash rate on every nonce, so update after after 2^X nonces
   140  			attempts++
   141  			if (attempts % (1 << 15)) == 0 {
   142  				blake3pow.hashrate.Mark(attempts)
   143  				attempts = 0
   144  			}
   145  			// Compute the PoW value of this nonce
   146  			header = types.CopyHeader(header)
   147  			header.SetNonce(types.EncodeNonce(nonce))
   148  			hash := header.Hash().Bytes()
   149  			if powBuffer.SetBytes(hash).Cmp(target) <= 0 {
   150  				// Correct nonce found, create a new header with it
   151  
   152  				// Seal and return a block (if still needed)
   153  				select {
   154  				case found <- header:
   155  					logger.Trace("Blake3pow nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
   156  				case <-abort:
   157  					logger.Trace("Blake3pow nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
   158  				}
   159  				break search
   160  			}
   161  			nonce++
   162  		}
   163  	}
   164  }
   165  
   166  // This is the timeout for HTTP requests to notify external miners.
   167  const remoteSealerTimeout = 1 * time.Second
   168  
   169  type remoteSealer struct {
   170  	works         map[common.Hash]*types.Header
   171  	rates         map[common.Hash]hashrate
   172  	currentHeader *types.Header
   173  	currentWork   [4]string
   174  	notifyCtx     context.Context
   175  	cancelNotify  context.CancelFunc // cancels all notification requests
   176  	reqWG         sync.WaitGroup     // tracks notification request goroutines
   177  
   178  	blake3pow    *Blake3pow
   179  	noverify     bool
   180  	notifyURLs   []string
   181  	results      chan<- *types.Header
   182  	workCh       chan *sealTask   // Notification channel to push new work and relative result channel to remote sealer
   183  	fetchWorkCh  chan *sealWork   // Channel used for remote sealer to fetch mining work
   184  	submitWorkCh chan *mineResult // Channel used for remote sealer to submit their mining result
   185  	fetchRateCh  chan chan uint64 // Channel used to gather submitted hash rate for local or remote sealer.
   186  	submitRateCh chan *hashrate   // Channel used for remote sealer to submit their mining hashrate
   187  	requestExit  chan struct{}
   188  	exitCh       chan struct{}
   189  }
   190  
   191  // sealTask wraps a seal header with relative result channel for remote sealer thread.
   192  type sealTask struct {
   193  	header  *types.Header
   194  	results chan<- *types.Header
   195  }
   196  
   197  // mineResult wraps the pow solution parameters for the specified block.
   198  type mineResult struct {
   199  	nonce types.BlockNonce
   200  	hash  common.Hash
   201  
   202  	errc chan error
   203  }
   204  
   205  // hashrate wraps the hash rate submitted by the remote sealer.
   206  type hashrate struct {
   207  	id   common.Hash
   208  	ping time.Time
   209  	rate uint64
   210  
   211  	done chan struct{}
   212  }
   213  
   214  // sealWork wraps a seal work package for remote sealer.
   215  type sealWork struct {
   216  	errc chan error
   217  	res  chan [4]string
   218  }
   219  
   220  func startRemoteSealer(blake3pow *Blake3pow, urls []string, noverify bool) *remoteSealer {
   221  	ctx, cancel := context.WithCancel(context.Background())
   222  	s := &remoteSealer{
   223  		blake3pow:    blake3pow,
   224  		noverify:     noverify,
   225  		notifyURLs:   urls,
   226  		notifyCtx:    ctx,
   227  		cancelNotify: cancel,
   228  		works:        make(map[common.Hash]*types.Header),
   229  		rates:        make(map[common.Hash]hashrate),
   230  		workCh:       make(chan *sealTask),
   231  		fetchWorkCh:  make(chan *sealWork),
   232  		submitWorkCh: make(chan *mineResult),
   233  		fetchRateCh:  make(chan chan uint64),
   234  		submitRateCh: make(chan *hashrate),
   235  		requestExit:  make(chan struct{}),
   236  		exitCh:       make(chan struct{}),
   237  	}
   238  	go s.loop()
   239  	return s
   240  }
   241  
   242  func (s *remoteSealer) loop() {
   243  	defer func() {
   244  		s.blake3pow.config.Log.Trace("Blake3pow remote sealer is exiting")
   245  		s.cancelNotify()
   246  		s.reqWG.Wait()
   247  		close(s.exitCh)
   248  	}()
   249  
   250  	ticker := time.NewTicker(5 * time.Second)
   251  	defer ticker.Stop()
   252  
   253  	for {
   254  		select {
   255  		case work := <-s.workCh:
   256  			// Update current work with new received header.
   257  			// Note same work can be past twice, happens when changing CPU threads.
   258  			s.results = work.results
   259  			s.makeWork(work.header)
   260  			s.notifyWork()
   261  
   262  		case work := <-s.fetchWorkCh:
   263  			// Return current mining work to remote miner.
   264  			if s.currentHeader == nil {
   265  				work.errc <- errNoMiningWork
   266  			} else {
   267  				work.res <- s.currentWork
   268  			}
   269  
   270  		case result := <-s.submitWorkCh:
   271  			// Verify submitted PoW solution based on maintained mining blocks.
   272  			if s.submitWork(result.nonce, result.hash) {
   273  				result.errc <- nil
   274  			} else {
   275  				result.errc <- errInvalidSealResult
   276  			}
   277  
   278  		case result := <-s.submitRateCh:
   279  			// Trace remote sealer's hash rate by submitted value.
   280  			s.rates[result.id] = hashrate{rate: result.rate, ping: time.Now()}
   281  			close(result.done)
   282  
   283  		case req := <-s.fetchRateCh:
   284  			// Gather all hash rate submitted by remote sealer.
   285  			var total uint64
   286  			for _, rate := range s.rates {
   287  				// this could overflow
   288  				total += rate.rate
   289  			}
   290  			req <- total
   291  
   292  		case <-ticker.C:
   293  			// Clear stale submitted hash rate.
   294  			for id, rate := range s.rates {
   295  				if time.Since(rate.ping) > 10*time.Second {
   296  					delete(s.rates, id)
   297  				}
   298  			}
   299  			// Clear stale pending blocks
   300  			if s.currentHeader != nil {
   301  				for hash, header := range s.works {
   302  					if header.NumberU64()+staleThreshold <= s.currentHeader.NumberU64() {
   303  						delete(s.works, hash)
   304  					}
   305  				}
   306  			}
   307  
   308  		case <-s.requestExit:
   309  			return
   310  		}
   311  	}
   312  }
   313  
   314  // makeWork creates a work package for external miner.
   315  //
   316  // The work package consists of 3 strings:
   317  //
   318  //	result[0], 32 bytes hex encoded current header pow-hash
   319  //	result[1], 32 bytes hex encoded seed hash used for DAG
   320  //	result[2], 32 bytes hex encoded boundary condition ("target"), 2^256/difficulty
   321  //	result[3], hex encoded header number
   322  func (s *remoteSealer) makeWork(header *types.Header) {
   323  	hash := header.SealHash()
   324  	s.currentWork[0] = hash.Hex()
   325  	s.currentWork[1] = hexutil.EncodeBig(header.Number())
   326  	s.currentWork[2] = common.BytesToHash(new(big.Int).Div(big2e256, header.Difficulty()).Bytes()).Hex()
   327  
   328  	// Trace the seal work fetched by remote sealer.
   329  	s.currentHeader = header
   330  	s.works[hash] = header
   331  }
   332  
   333  // notifyWork notifies all the specified mining endpoints of the availability of
   334  // new work to be processed.
   335  func (s *remoteSealer) notifyWork() {
   336  	work := s.currentWork
   337  
   338  	// Encode the JSON payload of the notification. When NotifyFull is set,
   339  	// this is the complete block header, otherwise it is a JSON array.
   340  	var blob []byte
   341  	if s.blake3pow.config.NotifyFull {
   342  		blob, _ = json.Marshal(s.currentHeader)
   343  	} else {
   344  		blob, _ = json.Marshal(work)
   345  	}
   346  
   347  	s.reqWG.Add(len(s.notifyURLs))
   348  	for _, url := range s.notifyURLs {
   349  		go s.sendNotification(s.notifyCtx, url, blob, work)
   350  	}
   351  }
   352  
   353  func (s *remoteSealer) sendNotification(ctx context.Context, url string, json []byte, work [4]string) {
   354  	defer s.reqWG.Done()
   355  
   356  	req, err := http.NewRequest("POST", url, bytes.NewReader(json))
   357  	if err != nil {
   358  		s.blake3pow.config.Log.Warn("Can't create remote miner notification", "err", err)
   359  		return
   360  	}
   361  	ctx, cancel := context.WithTimeout(ctx, remoteSealerTimeout)
   362  	defer cancel()
   363  	req = req.WithContext(ctx)
   364  	req.Header.Set("Content-Type", "application/json")
   365  
   366  	resp, err := http.DefaultClient.Do(req)
   367  	if err != nil {
   368  		s.blake3pow.config.Log.Warn("Failed to notify remote miner", "err", err)
   369  	} else {
   370  		s.blake3pow.config.Log.Trace("Notified remote miner", "miner", url, "hash", work[0], "target", work[2])
   371  		resp.Body.Close()
   372  	}
   373  }
   374  
   375  // submitWork verifies the submitted pow solution, returning
   376  // whether the solution was accepted or not (not can be both a bad pow as well as
   377  // any other error, like no pending work or stale mining result).
   378  func (s *remoteSealer) submitWork(nonce types.BlockNonce, sealhash common.Hash) bool {
   379  	if s.currentHeader == nil {
   380  		s.blake3pow.config.Log.Error("Pending work without block", "sealhash", sealhash)
   381  		return false
   382  	}
   383  	// Make sure the work submitted is present
   384  	header := s.works[sealhash]
   385  	if header == nil {
   386  		s.blake3pow.config.Log.Warn("Work submitted but none pending", "sealhash", sealhash, "curnumber", s.currentHeader.NumberU64())
   387  		return false
   388  	}
   389  	// Verify the correctness of submitted result.
   390  	header.SetNonce(nonce)
   391  
   392  	start := time.Now()
   393  	// Make sure the result channel is assigned.
   394  	if s.results == nil {
   395  		s.blake3pow.config.Log.Warn("Blake3pow result channel is empty, submitted mining result is rejected")
   396  		return false
   397  	}
   398  	s.blake3pow.config.Log.Trace("Verified correct proof-of-work", "sealhash", sealhash, "elapsed", common.PrettyDuration(time.Since(start)))
   399  
   400  	// Solutions seems to be valid, return to the miner and notify acceptance.
   401  	solution := header
   402  
   403  	// The submitted solution is within the scope of acceptance.
   404  	if solution.NumberU64()+staleThreshold > s.currentHeader.NumberU64() {
   405  		select {
   406  		case s.results <- solution:
   407  			s.blake3pow.config.Log.Debug("Work submitted is acceptable", "number", solution.NumberU64(), "sealhash", sealhash, "hash", solution.Hash())
   408  			return true
   409  		default:
   410  			s.blake3pow.config.Log.Warn("Sealing result is not read by miner", "mode", "remote", "sealhash", sealhash)
   411  			return false
   412  		}
   413  	}
   414  	// The submitted block is too old to accept, drop it.
   415  	s.blake3pow.config.Log.Warn("Work submitted is too old", "number", solution.NumberU64(), "sealhash", sealhash, "hash", solution.Hash())
   416  	return false
   417  }