Skip to content
This repository has been archived by the owner on Oct 4, 2019. It is now read-only.

Commit

Permalink
EIP 100 Difficulty adjustment and testing (#36)
Browse files Browse the repository at this point in the history
* Set up testing framework for difficulty

* Set up framework for testing difficulty

* Implemented EIP 100 and set up testing config

* Set up testing framework for difficulty

* Set up framework for testing difficulty

* Implemented EIP 100 and set up testing config

* Cleaned up and moved params to file

* Fixed usages of CalcDifficulty

* Moved parsing of hex or decimal strings functions to common package
  • Loading branch information
austinabell authored May 24, 2019
1 parent 08ef773 commit fe17e9e
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 12 deletions.
31 changes: 31 additions & 0 deletions common/hexutil/hexutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,34 @@ func mapError(err error) error {
}
return err
}

// HexOrDecimalToUint64 parses ambiguous string of hex/decimal into *big.Int
func HexOrDecimalToUint64(s string) (uint64, bool) {
if s == "" {
return 0, true
}
if len(s) >= 2 && (s[:2] == "0x" || s[:2] == "0X") {
v, err := strconv.ParseUint(s[2:], 16, 64)
return v, err == nil
}
v, err := strconv.ParseUint(s, 10, 64)
return v, err == nil
}

// HexOrDecimalToBigInt parses ambiguous string of hex/decimal into *big.Int
func HexOrDecimalToBigInt(s string) (*big.Int, bool) {
if s == "" {
return new(big.Int), true
}
var bigint *big.Int
var ok bool
if len(s) >= 2 && (s[:2] == "0x" || s[:2] == "0X") {
bigint, ok = new(big.Int).SetString(s[2:], 16)
} else {
bigint, ok = new(big.Int).SetString(s, 10)
}
if ok && bigint.BitLen() > 256 {
bigint, ok = nil, false
}
return bigint, ok
}
82 changes: 75 additions & 7 deletions core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/eth-classic/go-ethereum/core/state"
"github.com/eth-classic/go-ethereum/core/types"
"github.com/eth-classic/go-ethereum/logger/glog"
"github.com/eth-classic/go-ethereum/params"
"github.com/eth-classic/go-ethereum/pow"
"gopkg.in/fatih/set.v0"
)
Expand All @@ -39,11 +40,6 @@ var (
GasLimitBoundDivisor = big.NewInt(1024) // The bound divisor of the gas limit, used in update calculations.
)

var (
big10 = big.NewInt(10)
bigMinus99 = big.NewInt(-99)
)

// Difficulty allows passing configurable options to a given difficulty algorithm.
type DifficultyConfig struct {
Name string `json:"name"`
Expand Down Expand Up @@ -235,7 +231,7 @@ func ValidateHeader(config *ChainConfig, pow pow.PoW, header *types.Header, pare
return BlockEqualTSErr
}

expd := CalcDifficulty(config, header.Time.Uint64(), parent.Time.Uint64(), parent.Number, parent.Difficulty)
expd := CalcDifficulty(config, header.Time.Uint64(), parent)
if expd.Cmp(header.Difficulty) != 0 {
return fmt.Errorf("Difficulty check failed for header %v != %v at %v", header.Difficulty, expd, header.Number)
}
Expand Down Expand Up @@ -268,7 +264,11 @@ func ValidateHeader(config *ChainConfig, pow pow.PoW, header *types.Header, pare
// CalcDifficulty is the difficulty adjustment algorithm. It returns
// the difficulty that a new block should have when created at time
// given the parent block's time and difficulty.
func CalcDifficulty(config *ChainConfig, time, parentTime uint64, parentNumber, parentDiff *big.Int) *big.Int {
func CalcDifficulty(config *ChainConfig, time uint64, parent *types.Header) *big.Int {
parentTime := parent.Time.Uint64()
parentNumber := parent.Number
parentDiff := parent.Difficulty

if config == nil {
glog.Fatalln("missing chain configuration, cannot calculate difficulty")
}
Expand All @@ -286,6 +286,8 @@ func CalcDifficulty(config *ChainConfig, time, parentTime uint64, parentNumber,
name = ""
} // will fall to default panic
switch name {
case "atlantis":
return calcDifficultyAtlantis(time, parent)
case "defused":
return calcDifficultyDefused(time, parentTime, parentNumber, parentDiff)
case "ecip1010":
Expand All @@ -310,6 +312,72 @@ func CalcDifficulty(config *ChainConfig, time, parentTime uint64, parentNumber,
}
}

// Some weird constants to avoid constant memory allocs for them.
var (
expDiffPeriod = big.NewInt(100000)
big1 = big.NewInt(1)
big2 = big.NewInt(2)
big9 = big.NewInt(9)
big10 = big.NewInt(10)
bigMinus99 = big.NewInt(-99)
)

func calcDifficultyAtlantis(time uint64, parent *types.Header) *big.Int {
bombDelayFromParent := new(big.Int).Sub(big.NewInt(3000000), big1)
// https://github.com/ethereum/EIPs/issues/100.
// algorithm:
// diff = (parent_diff +
// (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
// ) + 2^(periodCount - 2)

bigTime := new(big.Int).SetUint64(time)
bigParentTime := parent.Time

// holds intermediate values to make the algo easier to read & audit
x := new(big.Int)
y := new(big.Int)

// (2 if len(parent_uncles) else 1) - (block_timestamp - parent_timestamp) // 9
x.Sub(bigTime, bigParentTime)
x.Div(x, big9)
if parent.UncleHash == types.EmptyUncleHash {
x.Sub(big1, x)
} else {
x.Sub(big2, x)
}
// max((2 if len(parent_uncles) else 1) - (block_timestamp - parent_timestamp) // 9, -99)
if x.Cmp(bigMinus99) < 0 {
x.Set(bigMinus99)
}
// parent_diff + (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
y.Div(parent.Difficulty, params.DifficultyBoundDivisor)
x.Mul(y, x)
x.Add(parent.Difficulty, x)

// minimum difficulty can ever be (before exponential factor)
if x.Cmp(params.MinimumDifficulty) < 0 {
x.Set(params.MinimumDifficulty)
}
// calculate a fake block number for the ice-age delay
// Specification: https://eips.ethereum.org/EIPS/eip-1234
fakeBlockNumber := new(big.Int)
if parent.Number.Cmp(bombDelayFromParent) >= 0 {
fakeBlockNumber = fakeBlockNumber.Sub(parent.Number, bombDelayFromParent)
}
// for the exponential factor
periodCount := fakeBlockNumber
periodCount.Div(periodCount, expDiffPeriod)

// the exponential factor, commonly referred to as "the bomb"
// diff = diff + 2^(periodCount - 2)
if periodCount.Cmp(big1) > 0 {
y.Sub(periodCount, big2)
y.Exp(big2, y, nil)
x.Add(x, y)
}
return x
}

func calcDifficultyDiehard(time, parentTime uint64, parentDiff *big.Int, diehardBlock *big.Int) *big.Int {
// https://github.com/eth-classic/ECIPs/blob/master/ECIPS/ECIP-1010.md
// algorithm:
Expand Down
16 changes: 14 additions & 2 deletions core/block_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,13 @@ func TestCalcDifficulty1Mainnet(t *testing.T) {
}

for parentNum, expected := range table {
difficulty := CalcDifficulty(config, time, parentTime, parentNum, parentDiff)
parent := &types.Header{
Number: parentNum,
Time: new(big.Int).SetUint64(parentTime),
Difficulty: parentDiff,
}

difficulty := CalcDifficulty(config, time, parent)
if difficulty.Cmp(expected) != 0 {
t.Errorf("config: %v, got: %v, want: %v, with parentBlock: %v", "mainnet", difficulty, expected, parentNum)
}
Expand Down Expand Up @@ -467,7 +473,13 @@ func TestCalcDifficulty1Morden(t *testing.T) {
}

for parentNum, expected := range table {
difficulty := CalcDifficulty(config, time, parentTime, parentNum, parentDiff)
parent := &types.Header{
Number: parentNum,
Time: new(big.Int).SetUint64(parentTime),
Difficulty: parentDiff,
}

difficulty := CalcDifficulty(config, time, parent)
if difficulty.Cmp(expected) != 0 {
t.Errorf("config: %v, got: %v, want: %v, with parentBlock: %v", "mainnet", difficulty, expected, parentNum)
}
Expand Down
4 changes: 2 additions & 2 deletions core/chain_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (b *BlockGen) OffsetTime(seconds int64) {
if b.header.Time.Cmp(b.parent.Header().Time) <= 0 {
panic("block time out of range")
}
b.header.Difficulty = CalcDifficulty(MakeChainConfig(), b.header.Time.Uint64(), b.parent.Time().Uint64(), b.parent.Number(), b.parent.Difficulty())
b.header.Difficulty = CalcDifficulty(MakeChainConfig(), b.header.Time.Uint64(), b.parent.Header())
}

// GenerateChain creates a chain of n blocks. The first block's
Expand Down Expand Up @@ -266,7 +266,7 @@ func makeHeader(config *ChainConfig, parent *types.Block, state *state.StateDB)
Root: state.IntermediateRoot(false),
ParentHash: parent.Hash(),
Coinbase: parent.Coinbase(),
Difficulty: CalcDifficulty(config, time.Uint64(), new(big.Int).Sub(time, big.NewInt(10)).Uint64(), parent.Number(), parent.Difficulty()),
Difficulty: CalcDifficulty(config, time.Uint64(), parent.Header()),
GasLimit: CalcGasLimit(parent),
GasUsed: new(big.Int),
Number: new(big.Int).Add(parent.Number(), common.Big1),
Expand Down
2 changes: 1 addition & 1 deletion miner/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ func (self *worker) commitNewWork() {
header := &types.Header{
ParentHash: parent.Hash(),
Number: num.Add(num, common.Big1),
Difficulty: core.CalcDifficulty(self.config, uint64(tstamp), parent.Time().Uint64(), parent.Number(), parent.Difficulty()),
Difficulty: core.CalcDifficulty(self.config, uint64(tstamp), parent.Header()),
GasLimit: core.CalcGasLimit(parent),
GasUsed: new(big.Int),
Coinbase: self.coinbase,
Expand Down
8 changes: 8 additions & 0 deletions params/protocol_params.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package params

import "math/big"

const (
QuadCoeffDiv uint64 = 512 // Divisor for the quadratic particle of the memory cost equation.

StackLimit uint64 = 1024 // Maximum size of VM stack allowed.
MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL.
)

var (
DifficultyBoundDivisor = big.NewInt(2048) // The bound divisor of the difficulty, used in the update calculations.

MinimumDifficulty = big.NewInt(131072) // The minimum that the difficulty may ever be.
)
63 changes: 63 additions & 0 deletions tests/difficulty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2015 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 tests

import (
"path/filepath"
"strings"
"testing"
)

func TestETHDifficulty(t *testing.T) {
fileNames, _ := filepath.Glob(filepath.Join(ethBasicTestDir, "*"))

supportedTests := map[string]bool{
// "difficulty.json": true, // Testing ETH mainnet config
"difficultyFrontier.json": true,
"difficultyHomestead.json": true,
"difficultyByzantium.json": true,
}

// Loop through each file
for _, fn := range fileNames {
fileName := fn[strings.LastIndex(fn, "/")+1 : len(fn)]

if !supportedTests[fileName] {
continue
}

t.Run(fileName, func(t *testing.T) {
config := ChainConfigs[fileName]
tests := make(map[string]DifficultyTest)

if err := readJsonFile(fn, &tests); err != nil {
t.Error(err)
}

// Loop through each test in file
for key, test := range tests {
// Subtest within the JSON file
t.Run(key, func(t *testing.T) {
if err := test.runDifficulty(t, &config); err != nil {
t.Error(err)
}
})

}
})
}
}
71 changes: 71 additions & 0 deletions tests/difficulty_test_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2015 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 tests

import (
"fmt"
"math/big"
"testing"

"github.com/eth-classic/go-ethereum/common"
"github.com/eth-classic/go-ethereum/common/hexutil"
"github.com/eth-classic/go-ethereum/core"
"github.com/eth-classic/go-ethereum/core/types"
"github.com/eth-classic/go-ethereum/params"
)

// DifficultyTest is the structure of JSON from test files
type DifficultyTest struct {
ParentTimestamp string `json:"parentTimestamp"`
ParentDifficulty string `json:"parentDifficulty"`
UncleHash common.Hash `json:"parentUncles"`
CurrentTimestamp string `json:"currentTimestamp"`
CurrentBlockNumber string `json:"currentBlockNumber"`
CurrentDifficulty string `json:"currentDifficulty"`
}

func (test *DifficultyTest) runDifficulty(t *testing.T, config *core.ChainConfig) error {
currentNumber, _ := hexutil.HexOrDecimalToBigInt(test.CurrentBlockNumber)
parentNumber := new(big.Int).Sub(currentNumber, big.NewInt(1))
parentTimestamp, _ := hexutil.HexOrDecimalToBigInt(test.ParentTimestamp)
parentDifficulty, _ := hexutil.HexOrDecimalToBigInt(test.ParentDifficulty)
currentTimestamp, _ := hexutil.HexOrDecimalToUint64(test.CurrentTimestamp)

parent := &types.Header{
Number: parentNumber,
Time: parentTimestamp,
Difficulty: parentDifficulty,
UncleHash: test.UncleHash,
}

// Check to make sure difficulty is above minimum
if parentDifficulty.Cmp(params.MinimumDifficulty) < 0 {
t.Skip("difficulty below minimum")
return nil
}

actual := core.CalcDifficulty(config, currentTimestamp, parent)
exp, _ := hexutil.HexOrDecimalToBigInt(test.CurrentDifficulty)

if actual.Cmp(exp) != 0 {
return fmt.Errorf("parent[time %v diff %v unclehash:%x] child[time %v number %v] diff %v != expected %v",
test.ParentTimestamp, test.ParentDifficulty, test.UncleHash,
test.CurrentTimestamp, test.CurrentBlockNumber, actual, exp)
}
return nil

}
Loading

0 comments on commit fe17e9e

Please sign in to comment.