Skip to content

Commit efe6dd2

Browse files
authored
consensus/ethash: implement faster difficulty calculators (#21976)
This PR adds re-written difficulty calculators, which are based on uint256. It also adds a fuzzer + oss-fuzz integration for the new fuzzer. It does differential fuzzing between the new and old calculators. Note: this PR does not actually enable the new calculators.
1 parent 88c6962 commit efe6dd2

File tree

6 files changed

+469
-0
lines changed

6 files changed

+469
-0
lines changed

consensus/ethash/consensus.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,11 @@ func calcDifficultyFrontier(time uint64, parent *types.Header) *big.Int {
485485
return diff
486486
}
487487

488+
// Exported for fuzzing
489+
var FrontierDifficultyCalulator = calcDifficultyFrontier
490+
var HomesteadDifficultyCalulator = calcDifficultyHomestead
491+
var DynamicDifficultyCalculator = makeDifficultyCalculator
492+
488493
// VerifySeal implements consensus.Engine, checking whether the given block satisfies
489494
// the PoW difficulty requirements.
490495
func (ethash *Ethash) VerifySeal(chain consensus.ChainHeaderReader, header *types.Header) error {

consensus/ethash/consensus_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
package ethash
1818

1919
import (
20+
"encoding/binary"
2021
"encoding/json"
2122
"math/big"
23+
"math/rand"
2224
"os"
2325
"path/filepath"
2426
"testing"
2527

28+
"github.com/ethereum/go-ethereum/common"
2629
"github.com/ethereum/go-ethereum/common/math"
2730
"github.com/ethereum/go-ethereum/core/types"
2831
"github.com/ethereum/go-ethereum/params"
@@ -84,3 +87,102 @@ func TestCalcDifficulty(t *testing.T) {
8487
}
8588
}
8689
}
90+
91+
func randSlice(min, max uint32) []byte {
92+
var b = make([]byte, 4)
93+
rand.Read(b)
94+
a := binary.LittleEndian.Uint32(b)
95+
size := min + a%(max-min)
96+
out := make([]byte, size)
97+
rand.Read(out)
98+
return out
99+
}
100+
101+
func TestDifficultyCalculators(t *testing.T) {
102+
rand.Seed(2)
103+
for i := 0; i < 5000; i++ {
104+
// 1 to 300 seconds diff
105+
var timeDelta = uint64(1 + rand.Uint32()%3000)
106+
diffBig := big.NewInt(0).SetBytes(randSlice(2, 10))
107+
if diffBig.Cmp(params.MinimumDifficulty) < 0 {
108+
diffBig.Set(params.MinimumDifficulty)
109+
}
110+
//rand.Read(difficulty)
111+
header := &types.Header{
112+
Difficulty: diffBig,
113+
Number: new(big.Int).SetUint64(rand.Uint64() % 50_000_000),
114+
Time: rand.Uint64() - timeDelta,
115+
}
116+
if rand.Uint32()&1 == 0 {
117+
header.UncleHash = types.EmptyUncleHash
118+
}
119+
bombDelay := new(big.Int).SetUint64(rand.Uint64() % 50_000_000)
120+
for i, pair := range []struct {
121+
bigFn func(time uint64, parent *types.Header) *big.Int
122+
u256Fn func(time uint64, parent *types.Header) *big.Int
123+
}{
124+
{FrontierDifficultyCalulator, CalcDifficultyFrontierU256},
125+
{HomesteadDifficultyCalulator, CalcDifficultyHomesteadU256},
126+
{DynamicDifficultyCalculator(bombDelay), MakeDifficultyCalculatorU256(bombDelay)},
127+
} {
128+
time := header.Time + timeDelta
129+
want := pair.bigFn(time, header)
130+
have := pair.u256Fn(time, header)
131+
if want.BitLen() > 256 {
132+
continue
133+
}
134+
if want.Cmp(have) != 0 {
135+
t.Fatalf("pair %d: want %x have %x\nparent.Number: %x\np.Time: %x\nc.Time: %x\nBombdelay: %v\n", i, want, have,
136+
header.Number, header.Time, time, bombDelay)
137+
}
138+
}
139+
}
140+
}
141+
142+
func BenchmarkDifficultyCalculator(b *testing.B) {
143+
x1 := makeDifficultyCalculator(big.NewInt(1000000))
144+
x2 := MakeDifficultyCalculatorU256(big.NewInt(1000000))
145+
h := &types.Header{
146+
ParentHash: common.Hash{},
147+
UncleHash: types.EmptyUncleHash,
148+
Difficulty: big.NewInt(0xffffff),
149+
Number: big.NewInt(500000),
150+
Time: 1000000,
151+
}
152+
b.Run("big-frontier", func(b *testing.B) {
153+
b.ReportAllocs()
154+
for i := 0; i < b.N; i++ {
155+
calcDifficultyFrontier(1000014, h)
156+
}
157+
})
158+
b.Run("u256-frontier", func(b *testing.B) {
159+
b.ReportAllocs()
160+
for i := 0; i < b.N; i++ {
161+
CalcDifficultyFrontierU256(1000014, h)
162+
}
163+
})
164+
b.Run("big-homestead", func(b *testing.B) {
165+
b.ReportAllocs()
166+
for i := 0; i < b.N; i++ {
167+
calcDifficultyHomestead(1000014, h)
168+
}
169+
})
170+
b.Run("u256-homestead", func(b *testing.B) {
171+
b.ReportAllocs()
172+
for i := 0; i < b.N; i++ {
173+
CalcDifficultyHomesteadU256(1000014, h)
174+
}
175+
})
176+
b.Run("big-generic", func(b *testing.B) {
177+
b.ReportAllocs()
178+
for i := 0; i < b.N; i++ {
179+
x1(1000014, h)
180+
}
181+
})
182+
b.Run("u256-generic", func(b *testing.B) {
183+
b.ReportAllocs()
184+
for i := 0; i < b.N; i++ {
185+
x2(1000014, h)
186+
}
187+
})
188+
}

consensus/ethash/difficulty.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Copyright 2020 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package ethash
18+
19+
import (
20+
"math/big"
21+
22+
"github.com/ethereum/go-ethereum/core/types"
23+
"github.com/holiman/uint256"
24+
)
25+
26+
const (
27+
// frontierDurationLimit is for Frontier:
28+
// The decision boundary on the blocktime duration used to determine
29+
// whether difficulty should go up or down.
30+
frontierDurationLimit = 13
31+
// minimumDifficulty The minimum that the difficulty may ever be.
32+
minimumDifficulty = 131072
33+
// expDiffPeriod is the exponential difficulty period
34+
expDiffPeriodUint = 100000
35+
// difficultyBoundDivisorBitShift is the bound divisor of the difficulty (2048),
36+
// This constant is the right-shifts to use for the division.
37+
difficultyBoundDivisor = 11
38+
)
39+
40+
// CalcDifficultyFrontierU256 is the difficulty adjustment algorithm. It returns the
41+
// difficulty that a new block should have when created at time given the parent
42+
// block's time and difficulty. The calculation uses the Frontier rules.
43+
func CalcDifficultyFrontierU256(time uint64, parent *types.Header) *big.Int {
44+
/*
45+
Algorithm
46+
block_diff = pdiff + pdiff / 2048 * (1 if time - ptime < 13 else -1) + int(2^((num // 100000) - 2))
47+
48+
Where:
49+
- pdiff = parent.difficulty
50+
- ptime = parent.time
51+
- time = block.timestamp
52+
- num = block.number
53+
*/
54+
55+
pDiff := uint256.NewInt()
56+
pDiff.SetFromBig(parent.Difficulty) // pDiff: pdiff
57+
adjust := pDiff.Clone()
58+
adjust.Rsh(adjust, difficultyBoundDivisor) // adjust: pDiff / 2048
59+
60+
if time-parent.Time < frontierDurationLimit {
61+
pDiff.Add(pDiff, adjust)
62+
} else {
63+
pDiff.Sub(pDiff, adjust)
64+
}
65+
if pDiff.LtUint64(minimumDifficulty) {
66+
pDiff.SetUint64(minimumDifficulty)
67+
}
68+
// 'pdiff' now contains:
69+
// pdiff + pdiff / 2048 * (1 if time - ptime < 13 else -1)
70+
71+
if periodCount := (parent.Number.Uint64() + 1) / expDiffPeriodUint; periodCount > 1 {
72+
// diff = diff + 2^(periodCount - 2)
73+
expDiff := adjust.SetOne()
74+
expDiff.Lsh(expDiff, uint(periodCount-2)) // expdiff: 2 ^ (periodCount -2)
75+
pDiff.Add(pDiff, expDiff)
76+
}
77+
return pDiff.ToBig()
78+
}
79+
80+
// CalcDifficultyHomesteadU256 is the difficulty adjustment algorithm. It returns
81+
// the difficulty that a new block should have when created at time given the
82+
// parent block's time and difficulty. The calculation uses the Homestead rules.
83+
func CalcDifficultyHomesteadU256(time uint64, parent *types.Header) *big.Int {
84+
/*
85+
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2.md
86+
Algorithm:
87+
block_diff = pdiff + pdiff / 2048 * max(1 - (time - ptime) / 10, -99) + 2 ^ int((num / 100000) - 2))
88+
89+
Our modification, to use unsigned ints:
90+
block_diff = pdiff - pdiff / 2048 * max((time - ptime) / 10 - 1, 99) + 2 ^ int((num / 100000) - 2))
91+
92+
Where:
93+
- pdiff = parent.difficulty
94+
- ptime = parent.time
95+
- time = block.timestamp
96+
- num = block.number
97+
*/
98+
99+
pDiff := uint256.NewInt()
100+
pDiff.SetFromBig(parent.Difficulty) // pDiff: pdiff
101+
adjust := pDiff.Clone()
102+
adjust.Rsh(adjust, difficultyBoundDivisor) // adjust: pDiff / 2048
103+
104+
x := (time - parent.Time) / 10 // (time - ptime) / 10)
105+
var neg = true
106+
if x == 0 {
107+
x = 1
108+
neg = false
109+
} else if x >= 100 {
110+
x = 99
111+
} else {
112+
x = x - 1
113+
}
114+
z := new(uint256.Int).SetUint64(x)
115+
adjust.Mul(adjust, z) // adjust: (pdiff / 2048) * max((time - ptime) / 10 - 1, 99)
116+
if neg {
117+
pDiff.Sub(pDiff, adjust) // pdiff - pdiff / 2048 * max((time - ptime) / 10 - 1, 99)
118+
} else {
119+
pDiff.Add(pDiff, adjust) // pdiff + pdiff / 2048 * max((time - ptime) / 10 - 1, 99)
120+
}
121+
if pDiff.LtUint64(minimumDifficulty) {
122+
pDiff.SetUint64(minimumDifficulty)
123+
}
124+
// for the exponential factor, a.k.a "the bomb"
125+
// diff = diff + 2^(periodCount - 2)
126+
if periodCount := (1 + parent.Number.Uint64()) / expDiffPeriodUint; periodCount > 1 {
127+
expFactor := adjust.Lsh(adjust.SetOne(), uint(periodCount-2))
128+
pDiff.Add(pDiff, expFactor)
129+
}
130+
return pDiff.ToBig()
131+
}
132+
133+
// MakeDifficultyCalculatorU256 creates a difficultyCalculator with the given bomb-delay.
134+
// the difficulty is calculated with Byzantium rules, which differs from Homestead in
135+
// how uncles affect the calculation
136+
func MakeDifficultyCalculatorU256(bombDelay *big.Int) func(time uint64, parent *types.Header) *big.Int {
137+
// Note, the calculations below looks at the parent number, which is 1 below
138+
// the block number. Thus we remove one from the delay given
139+
bombDelayFromParent := bombDelay.Uint64() - 1
140+
return func(time uint64, parent *types.Header) *big.Int {
141+
/*
142+
https://github.com/ethereum/EIPs/issues/100
143+
pDiff = parent.difficulty
144+
BLOCK_DIFF_FACTOR = 9
145+
a = pDiff + (pDiff // BLOCK_DIFF_FACTOR) * adj_factor
146+
b = min(parent.difficulty, MIN_DIFF)
147+
child_diff = max(a,b )
148+
*/
149+
x := (time - parent.Time) / 9 // (block_timestamp - parent_timestamp) // 9
150+
c := uint64(1) // if parent.unclehash == emptyUncleHashHash
151+
if parent.UncleHash != types.EmptyUncleHash {
152+
c = 2
153+
}
154+
xNeg := x >= c
155+
if xNeg {
156+
// x is now _negative_ adjustment factor
157+
x = x - c // - ( (t-p)/p -( 2 or 1) )
158+
} else {
159+
x = c - x // (2 or 1) - (t-p)/9
160+
}
161+
if x > 99 {
162+
x = 99 // max(x, 99)
163+
}
164+
// parent_diff + (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
165+
y := new(uint256.Int)
166+
y.SetFromBig(parent.Difficulty) // y: p_diff
167+
pDiff := y.Clone() // pdiff: p_diff
168+
z := new(uint256.Int).SetUint64(x) //z : +-adj_factor (either pos or negative)
169+
y.Rsh(y, difficultyBoundDivisor) // y: p__diff / 2048
170+
z.Mul(y, z) // z: (p_diff / 2048 ) * (+- adj_factor)
171+
172+
if xNeg {
173+
y.Sub(pDiff, z) // y: parent_diff + parent_diff/2048 * adjustment_factor
174+
} else {
175+
y.Add(pDiff, z) // y: parent_diff + parent_diff/2048 * adjustment_factor
176+
}
177+
// minimum difficulty can ever be (before exponential factor)
178+
if y.LtUint64(minimumDifficulty) {
179+
y.SetUint64(minimumDifficulty)
180+
}
181+
// calculate a fake block number for the ice-age delay
182+
// Specification: https://eips.ethereum.org/EIPS/eip-1234
183+
var pNum = parent.Number.Uint64()
184+
if pNum >= bombDelayFromParent {
185+
if fakeBlockNumber := pNum - bombDelayFromParent; fakeBlockNumber >= 2*expDiffPeriodUint {
186+
z.SetOne()
187+
z.Lsh(z, uint(fakeBlockNumber/expDiffPeriodUint-2))
188+
y.Add(z, y)
189+
}
190+
}
191+
return y.ToBig()
192+
}
193+
}

oss-fuzz.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ compile_fuzzer tests/fuzzers/txfetcher Fuzz fuzzTxfetcher
5757
compile_fuzzer tests/fuzzers/rlp Fuzz fuzzRlp
5858
compile_fuzzer tests/fuzzers/trie Fuzz fuzzTrie
5959
compile_fuzzer tests/fuzzers/stacktrie Fuzz fuzzStackTrie
60+
compile_fuzzer tests/fuzzers/difficulty Fuzz fuzzDifficulty
6061

6162
compile_fuzzer tests/fuzzers/bls12381 FuzzG1Add fuzz_g1_add
6263
compile_fuzzer tests/fuzzers/bls12381 FuzzG1Mul fuzz_g1_mul
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
8+
"github.com/ethereum/go-ethereum/tests/fuzzers/difficulty"
9+
)
10+
11+
func main() {
12+
if len(os.Args) != 2 {
13+
fmt.Fprintf(os.Stderr, "Usage: debug <file>")
14+
os.Exit(1)
15+
}
16+
crasher := os.Args[1]
17+
data, err := ioutil.ReadFile(crasher)
18+
if err != nil {
19+
fmt.Fprintf(os.Stderr, "error loading crasher %v: %v", crasher, err)
20+
os.Exit(1)
21+
}
22+
difficulty.Fuzz(data)
23+
}

0 commit comments

Comments
 (0)