aboutsummaryrefslogtreecommitdiffstats
path: root/consensus/ethash/ethash.go
blob: ac049f9c36919a09a48d64eb66ac4242d4febfcb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

// Package ethash implements the ethash proof-of-work consensus engine.
package ethash

import (
    "errors"
    "fmt"
    "math"
    "math/big"
    "math/rand"
    "os"
    "path/filepath"
    "reflect"
    "runtime"
    "strconv"
    "sync"
    "time"
    "unsafe"

    mmap "github.com/edsrzf/mmap-go"
    "github.com/ethereum/go-ethereum/consensus"
    "github.com/ethereum/go-ethereum/log"
    "github.com/ethereum/go-ethereum/metrics"
    "github.com/ethereum/go-ethereum/rpc"
    "github.com/hashicorp/golang-lru/simplelru"
)

var ErrInvalidDumpMagic = errors.New("invalid dump magic")

var (
    // maxUint256 is a big integer representing 2^256-1
    maxUint256 = new(big.Int).Exp(big.NewInt(2), big.NewInt(256), big.NewInt(0))

    // sharedEthash is a full instance that can be shared between multiple users.
    sharedEthash = New(Config{"", 3, 0, "", 1, 0, ModeNormal})

    // algorithmRevision is the data structure version used for file naming.
    algorithmRevision = 23

    // dumpMagic is a dataset dump header to sanity check a data dump.
    dumpMagic = []uint32{0xbaddcafe, 0xfee1dead}
)

// isLittleEndian returns whether the local system is running in little or big
// endian byte order.
func isLittleEndian() bool {
    n := uint32(0x01020304)
    return *(*byte)(unsafe.Pointer(&n)) == 0x04
}

// memoryMap tries to memory map a file of uint32s for read only access.
func memoryMap(path string) (*os.File, mmap.MMap, []uint32, error) {
    file, err := os.OpenFile(path, os.O_RDONLY, 0644)
    if err != nil {
        return nil, nil, nil, err
    }
    mem, buffer, err := memoryMapFile(file, false)
    if err != nil {
        file.Close()
        return nil, nil, nil, err
    }
    for i, magic := range dumpMagic {
        if buffer[i] != magic {
            mem.Unmap()
            file.Close()
            return nil, nil, nil, ErrInvalidDumpMagic
        }
    }
    return file, mem, buffer[len(dumpMagic):], err
}

// memoryMapFile tries to memory map an already opened file descriptor.
func memoryMapFile(file *os.File, write bool) (mmap.MMap, []uint32, error) {
    // Try to memory map the file
    flag := mmap.RDONLY
    if write {
        flag = mmap.RDWR
    }
    mem, err := mmap.Map(file, flag, 0)
    if err != nil {
        return nil, nil, err
    }
    // Yay, we managed to memory map the file, here be dragons
    header := *(*reflect.SliceHeader)(unsafe.Pointer(&mem))
    header.Len /= 4
    header.Cap /= 4

    return mem, *(*[]uint32)(unsafe.Pointer(&header)), nil
}

// memoryMapAndGenerate tries to memory map a temporary file of uint32s for write
// access, fill it with the data from a generator and then move it into the final
// path requested.
func memoryMapAndGenerate(path string, size uint64, generator func(buffer []uint32)) (*os.File, mmap.MMap, []uint32, error) {
    // Ensure the data folder exists
    if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
        return nil, nil, nil, err
    }
    // Create a huge temporary empty file to fill with data
    temp := path + "." + strconv.Itoa(rand.Int())

    dump, err := os.Create(temp)
    if err != nil {
        return nil, nil, nil, err
    }
    if err = dump.Truncate(int64(len(dumpMagic))*4 + int64(size)); err != nil {
        return nil, nil, nil, err
    }
    // Memory map the file for writing and fill it with the generator
    mem, buffer, err := memoryMapFile(dump, true)
    if err != nil {
        dump.Close()
        return nil, nil, nil, err
    }
    copy(buffer, dumpMagic)

    data := buffer[len(dumpMagic):]
    generator(data)

    if err := mem.Unmap(); err != nil {
        return nil, nil, nil, err
    }
    if err := dump.Close(); err != nil {
        return nil, nil, nil, err
    }
    if err := os.Rename(temp, path); err != nil {
        return nil, nil, nil, err
    }
    return memoryMap(path)
}

// lru tracks caches or datasets by their last use time, keeping at most N of them.
type lru struct {
    what string
    new  func(epoch uint64) interface{}
    mu   sync.Mutex
    // Items are kept in a LRU cache, but there is a special case:
    // We always keep an item for (highest seen epoch) + 1 as the 'future item'.
    cache      *simplelru.LRU
    future     uint64
    futureItem interface{}
}

// newlru create a new least-recently-used cache for either the verification caches
// or the mining datasets.
func newlru(what string, maxItems int, new func(epoch uint64) interface{}) *lru {
    if maxItems <= 0 {
        maxItems = 1
    }
    cache, _ := simplelru.NewLRU(maxItems, func(key, value interface{}) {
        log.Trace("Evicted ethash "+what, "epoch", key)
    })
    return &lru{what: what, new: new, cache: cache}
}

// get retrieves or creates an item for the given epoch. The first return value is always
// non-nil. The second return value is non-nil if lru thinks that an item will be useful in
// the near future.
func (lru *lru) get(epoch uint64) (item, future interface{}) {
    lru.mu.Lock()
    defer lru.mu.Unlock()

    // Get or create the item for the requested epoch.
    item, ok := lru.cache.Get(epoch)
    if !ok {
        if lru.future > 0 && lru.future == epoch {
            item = lru.futureItem
        } else {
            log.Trace("Requiring new ethash "+lru.what, "epoch", epoch)
            item = lru.new(epoch)
        }
        lru.cache.Add(epoch, item)
    }
    // Update the 'future item' if epoch is larger than previously seen.
    if epoch < maxEpoch-1 && lru.future < epoch+1 {
        log.Trace("Requiring new future ethash "+lru.what, "epoch", epoch+1)
        future = lru.new(epoch + 1)
        lru.future = epoch + 1
        lru.futureItem = future
    }
    return item, future
}

// cache wraps an ethash cache with some metadata to allow easier concurrent use.
type cache struct {
    epoch uint64    // Epoch for which this cache is relevant
    dump  *os.File  // File descriptor of the memory mapped cache
    mmap  mmap.MMap // Memory map itself to unmap before releasing
    cache []uint32  // The actual cache data content (may be memory mapped)
    once  sync.Once // Ensures the cache is generated only once
}

// newCache creates a new ethash verification cache and returns it as a plain Go
// interface to be usable in an LRU cache.
func newCache(epoch uint64) interface{} {
    return &cache{epoch: epoch}
}

// generate ensures that the cache content is generated before use.
func (c *cache) generate(dir string, limit int, test bool) {
    c.once.Do(func() {
        size := cacheSize(c.epoch*epochLength + 1)
        seed := seedHash(c.epoch*epochLength + 1)
        if test {
            size = 1024
        }
        // If we don't store anything on disk, generate and return.
        if dir == "" {
            c.cache = make([]uint32, size/4)
            generateCache(c.cache, c.epoch, seed)
            return
        }
        // Disk storage is needed, this will get fancy
        var endian string
        if !isLittleEndian() {
            endian = ".be"
        }
        path := filepath.Join(dir, fmt.Sprintf("cache-R%d-%x%s", algorithmRevision, seed[:8], endian))
        logger := log.New("epoch", c.epoch)

        // We're about to mmap the file, ensure that the mapping is cleaned up when the
        // cache becomes unused.
        runtime.SetFinalizer(c, (*cache).finalizer)

        // Try to load the file from disk and memory map it
        var err error
        c.dump, c.mmap, c.cache, err = memoryMap(path)
        if err == nil {
            logger.Debug("Loaded old ethash cache from disk")
            return
        }
        logger.Debug("Failed to load old ethash cache", "err", err)

        // No previous cache available, create a new cache file to fill
        c.dump, c.mmap, c.cache, err = memoryMapAndGenerate(path, size, func(buffer []uint32) { generateCache(buffer, c.epoch, seed) })
        if err != nil {
            logger.Error("Failed to generate mapped ethash cache", "err", err)

            c.cache = make([]uint32, size/4)
            generateCache(c.cache, c.epoch, seed)
        }
        // Iterate over all previous instances and delete old ones
        for ep := int(c.epoch) - limit; ep >= 0; ep-- {
            seed := seedHash(uint64(ep)*epochLength + 1)
            path := filepath.Join(dir, fmt.Sprintf("cache-R%d-%x%s", algorithmRevision, seed[:8], endian))
            os.Remove(path)
        }
    })
}

// finalizer unmaps the memory and closes the file.
func (c *cache) finalizer() {
    if c.mmap != nil {
        c.mmap.Unmap()
        c.dump.Close()
        c.mmap, c.dump = nil, nil
    }
}

// dataset wraps an ethash dataset with some metadata to allow easier concurrent use.
type dataset struct {
    epoch   uint64    // Epoch for which this cache is relevant
    dump    *os.File  // File descriptor of the memory mapped cache
    mmap    mmap.MMap // Memory map itself to unmap before releasing
    dataset []uint32  // The actual cache data content
    once    sync.Once // Ensures the cache is generated only once
}

// newDataset creates a new ethash mining dataset and returns it as a plain Go
// interface to be usable in an LRU cache.
func newDataset(epoch uint64) interface{} {
    return &dataset{epoch: epoch}
}

// generate ensures that the dataset content is generated before use.
func (d *dataset) generate(dir string, limit int, test bool) {
    d.once.Do(func() {
        csize := cacheSize(d.epoch*epochLength + 1)
        dsize := datasetSize(d.epoch*epochLength + 1)
        seed := seedHash(d.epoch*epochLength + 1)
        if test {
            csize = 1024
            dsize = 32 * 1024
        }
        // If we don't store anything on disk, generate and return
        if dir == "" {
            cache := make([]uint32, csize/4)
            generateCache(cache, d.epoch, seed)

            d.dataset = make([]uint32, dsize/4)
            generateDataset(d.dataset, d.epoch, cache)
        }
        // Disk storage is needed, this will get fancy
        var endian string
        if !isLittleEndian() {
            endian = ".be"
        }
        path := filepath.Join(dir, fmt.Sprintf("full-R%d-%x%s", algorithmRevision, seed[:8], endian))
        logger := log.New("epoch", d.epoch)

        // We're about to mmap the file, ensure that the mapping is cleaned up when the
        // cache becomes unused.
        runtime.SetFinalizer(d, (*dataset).finalizer)

        // Try to load the file from disk and memory map it
        var err error
        d.dump, d.mmap, d.dataset, err = memoryMap(path)
        if err == nil {
            logger.Debug("Loaded old ethash dataset from disk")
            return
        }
        logger.Debug("Failed to load old ethash dataset", "err", err)

        // No previous dataset available, create a new dataset file to fill
        cache := make([]uint32, csize/4)
        generateCache(cache, d.epoch, seed)

        d.dump, d.mmap, d.dataset, err = memoryMapAndGenerate(path, dsize, func(buffer []uint32) { generateDataset(buffer, d.epoch, cache) })
        if err != nil {
            logger.Error("Failed to generate mapped ethash dataset", "err", err)

            d.dataset = make([]uint32, dsize/2)
            generateDataset(d.dataset, d.epoch, cache)
        }
        // Iterate over all previous instances and delete old ones
        for ep := int(d.epoch) - limit; ep >= 0; ep-- {
            seed := seedHash(uint64(ep)*epochLength + 1)
            path := filepath.Join(dir, fmt.Sprintf("full-R%d-%x%s", algorithmRevision, seed[:8], endian))
            os.Remove(path)
        }
    })
}

// finalizer closes any file handlers and memory maps open.
func (d *dataset) finalizer() {
    if d.mmap != nil {
        d.mmap.Unmap()
        d.dump.Close()
        d.mmap, d.dump = nil, nil
    }
}

// MakeCache generates a new ethash cache and optionally stores it to disk.
func MakeCache(block uint64, dir string) {
    c := cache{epoch: block / epochLength}
    c.generate(dir, math.MaxInt32, false)
}

// MakeDataset generates a new ethash dataset and optionally stores it to disk.
func MakeDataset(block uint64, dir string) {
    d := dataset{epoch: block / epochLength}
    d.generate(dir, math.MaxInt32, false)
}

// Mode defines the type and amount of PoW verification an ethash engine makes.
type Mode uint

const (
    ModeNormal Mode = iota
    ModeShared
    ModeTest
    ModeFake
    ModeFullFake
)

// Config are the configuration parameters of the ethash.
type Config struct {
    CacheDir       string
    CachesInMem    int
    CachesOnDisk   int
    DatasetDir     string
    DatasetsInMem  int
    DatasetsOnDisk int
    PowMode        Mode
}

// Ethash is a consensus engine based on proot-of-work implementing the ethash
// algorithm.
type Ethash struct {
    config Config

    caches   *lru // In memory caches to avoid regenerating too often
    datasets *lru // In memory datasets to avoid regenerating too often

    // Mining related fields
    rand     *rand.Rand    // Properly seeded random source for nonces
    threads  int           // Number of threads to mine on if mining
    update   chan struct{} // Notification channel to update mining parameters
    hashrate metrics.Meter // Meter tracking the average hashrate

    // The fields below are hooks for testing
    shared    *Ethash       // Shared PoW verifier to avoid cache regeneration
    fakeFail  uint64        // Block number which fails PoW check even in fake mode
    fakeDelay time.Duration // Time delay to sleep for before returning from verify

    lock sync.Mutex // Ensures thread safety for the in-memory caches and mining fields
}

// New creates a full sized ethash PoW scheme.
func New(config Config) *Ethash {
    if config.CachesInMem <= 0 {
        log.Warn("One ethash cache must always be in memory", "requested", config.CachesInMem)
        config.CachesInMem = 1
    }
    if config.CacheDir != "" && config.CachesOnDisk > 0 {
        log.Info("Disk storage enabled for ethash caches", "dir", config.CacheDir, "count", config.CachesOnDisk)
    }
    if config.DatasetDir != "" && config.DatasetsOnDisk > 0 {
        log.Info("Disk storage enabled for ethash DAGs", "dir", config.DatasetDir, "count", config.DatasetsOnDisk)
    }
    return &Ethash{
        config:   config,
        caches:   newlru("cache", config.CachesInMem, newCache),
        datasets: newlru("dataset", config.DatasetsInMem, newDataset),
        update:   make(chan struct{}),
        hashrate: metrics.NewMeter(),
    }
}

// NewTester creates a small sized ethash PoW scheme useful only for testing
// purposes.
func NewTester() *Ethash {
    return New(Config{CachesInMem: 1, PowMode: ModeTest})
}

// NewFaker creates a ethash consensus engine with a fake PoW scheme that accepts
// all blocks' seal as valid, though they still have to conform to the Ethereum
// consensus rules.
func NewFaker() *Ethash {
    return &Ethash{
        config: Config{
            PowMode: ModeFake,
        },
    }
}

// NewFakeFailer creates a ethash consensus engine with a fake PoW scheme that
// accepts all blocks as valid apart from the single one specified, though they
// still have to conform to the Ethereum consensus rules.
func NewFakeFailer(fail uint64) *Ethash {
    return &Ethash{
        config: Config{
            PowMode: ModeFake,
        },
        fakeFail: fail,
    }
}

// NewFakeDelayer creates a ethash consensus engine with a fake PoW scheme that
// accepts all blocks as valid, but delays verifications by some time, though
// they still have to conform to the Ethereum consensus rules.
func NewFakeDelayer(delay time.Duration) *Ethash {
    return &Ethash{
        config: Config{
            PowMode: ModeFake,
        },
        fakeDelay: delay,
    }
}

// NewFullFaker creates an ethash consensus engine with a full fake scheme that
// accepts all blocks as valid, without checking any consensus rules whatsoever.
func NewFullFaker() *Ethash {
    return &Ethash{
        config: Config{
            PowMode: ModeFullFake,
        },
    }
}

// NewShared creates a full sized ethash PoW shared between all requesters running
// in the same process.
func NewShared() *Ethash {
    return &Ethash{shared: sharedEthash}
}

// cache tries to retrieve a verification cache for the specified block number
// by first checking against a list of in-memory caches, then against caches
// stored on disk, and finally generating one if none can be found.
func (ethash *Ethash) cache(block uint64) *cache {
    epoch := block / epochLength
    currentI, futureI := ethash.caches.get(epoch)
    current := currentI.(*cache)

    // Wait for generation finish.
    current.generate(ethash.config.CacheDir, ethash.config.CachesOnDisk, ethash.config.PowMode == ModeTest)

    // If we need a new future cache, now's a good time to regenerate it.
    if futureI != nil {
        future := futureI.(*cache)
        go future.generate(ethash.config.CacheDir, ethash.config.CachesOnDisk, ethash.config.PowMode == ModeTest)
    }
    return current
}

// dataset tries to retrieve a mining dataset for the specified block number
// by first checking against a list of in-memory datasets, then against DAGs
// stored on disk, and finally generating one if none can be found.
func (ethash *Ethash) dataset(block uint64) *dataset {
    epoch := block / epochLength
    currentI, futureI := ethash.datasets.get(epoch)
    current := currentI.(*dataset)

    // Wait for generation finish.
    current.generate(ethash.config.DatasetDir, ethash.config.DatasetsOnDisk, ethash.config.PowMode == ModeTest)

    // If we need a new future dataset, now's a good time to regenerate it.
    if futureI != nil {
        future := futureI.(*dataset)
        go future.generate(ethash.config.DatasetDir, ethash.config.DatasetsOnDisk, ethash.config.PowMode == ModeTest)
    }

    return current
}

// Threads returns the number of mining threads currently enabled. This doesn't
// necessarily mean that mining is running!
func (ethash *Ethash) Threads() int {
    ethash.lock.Lock()
    defer ethash.lock.Unlock()

    return ethash.threads
}

// SetThreads updates the number of mining threads currently enabled. Calling
// this method does not start mining, only sets the thread count. If zero is
// specified, the miner will use all cores of the machine. Setting a thread
// count below zero is allowed and will cause the miner to idle, without any
// work being done.
func (ethash *Ethash) SetThreads(threads int) {
    ethash.lock.Lock()
    defer ethash.lock.Unlock()

    // If we're running a shared PoW, set the thread count on that instead
    if ethash.shared != nil {
        ethash.shared.SetThreads(threads)
        return
    }
    // Update the threads and ping any running seal to pull in any changes
    ethash.threads = threads
    select {
    case ethash.update <- struct{}{}:
    default:
    }
}

// Hashrate implements PoW, returning the measured rate of the search invocations
// per second over the last minute.
func (ethash *Ethash) Hashrate() float64 {
    return ethash.hashrate.Rate1()
}

// APIs implements consensus.Engine, returning the user facing RPC APIs. Currently
// that is empty.
func (ethash *Ethash) APIs(chain consensus.ChainReader) []rpc.API {
    return nil
}

// SeedHash is the seed to use for generating a verification cache and the mining
// dataset.
func SeedHash(block uint64) []byte {
    return seedHash(block)
}