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