Skip to content

Commit

Permalink
enforce backoff time for out-turn validator (ethereum#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
unclezoro authored Aug 7, 2020
1 parent 83d72b8 commit a8c9e53
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 41 deletions.
6 changes: 6 additions & 0 deletions cmd/geth/retesteth.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ type CParamsParams struct {
ConstantinopleForkBlock *math.HexOrDecimal64 `json:"constantinopleForkBlock"`
ConstantinopleFixForkBlock *math.HexOrDecimal64 `json:"constantinopleFixForkBlock"`
IstanbulBlock *math.HexOrDecimal64 `json:"istanbulForkBlock"`
RamanujanForkBlock *math.HexOrDecimal64 `json:"ramanujanForkBlock"`
ChainID *math.HexOrDecimal256 `json:"chainID"`
MaximumExtraDataSize math.HexOrDecimal64 `json:"maximumExtraDataSize"`
TieBreakingGas bool `json:"tieBreakingGas"`
Expand Down Expand Up @@ -322,6 +323,7 @@ func (api *RetestethAPI) SetChainParams(ctx context.Context, chainParams ChainPa
constantinopleBlock *big.Int
petersburgBlock *big.Int
istanbulBlock *big.Int
ramanujanBlock *big.Int
)
if chainParams.Params.HomesteadForkBlock != nil {
homesteadBlock = big.NewInt(int64(*chainParams.Params.HomesteadForkBlock))
Expand Down Expand Up @@ -351,6 +353,9 @@ func (api *RetestethAPI) SetChainParams(ctx context.Context, chainParams ChainPa
if chainParams.Params.IstanbulBlock != nil {
istanbulBlock = big.NewInt(int64(*chainParams.Params.IstanbulBlock))
}
if chainParams.Params.RamanujanForkBlock != nil {
ramanujanBlock = big.NewInt(int64(*chainParams.Params.RamanujanForkBlock))
}

genesis := &core.Genesis{
Config: &params.ChainConfig{
Expand All @@ -365,6 +370,7 @@ func (api *RetestethAPI) SetChainParams(ctx context.Context, chainParams ChainPa
ConstantinopleBlock: constantinopleBlock,
PetersburgBlock: petersburgBlock,
IstanbulBlock: istanbulBlock,
RamanujanBlock: ramanujanBlock,
},
Nonce: uint64(chainParams.Genesis.Nonce),
Timestamp: uint64(chainParams.Genesis.Timestamp),
Expand Down
53 changes: 37 additions & 16 deletions consensus/parlia/parlia.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ const (
extraSeal = 65 // Fixed number of extra-data suffix bytes reserved for signer seal

validatorBytesLength = common.AddressLength
wiggleTime = 500 * time.Millisecond // Random delay (per signer) to allow concurrent signers
fixedBackOffTime = 200 * time.Millisecond
wiggleTime = uint64(1) // second, Random delay (per signer) to allow concurrent signers
initialBackOffTime = uint64(1) // second

systemRewardPercent = 4 // it means 1/2^4 = 1/16 percentage of gas fee incoming will be distributed to system

Expand Down Expand Up @@ -81,7 +81,7 @@ var (
common.HexToAddress(GovHubContract): true,
common.HexToAddress(TokenHubContract): true,
common.HexToAddress(RelayerIncentivizeContract): true,
common.HexToAddress(CrossChainContract): true,
common.HexToAddress(CrossChainContract): true,
}
)

Expand Down Expand Up @@ -395,6 +395,16 @@ func (p *Parlia) verifyCascadingFields(chain consensus.ChainReader, header *type
return consensus.ErrUnknownAncestor
}

snap, err := p.snapshot(chain, number-1, header.ParentHash, parents)
if err != nil {
return err
}

err = p.blockTimeVerifyForRamanujanFork(snap, header, parent)
if err != nil {
return nil
}

// Verify that the gas limit is <= 2^63-1
capacity := uint64(0x7fffffffffffffff)
if header.GasLimit > capacity {
Expand Down Expand Up @@ -570,7 +580,7 @@ func (p *Parlia) verifySeal(chain consensus.ChainReader, header *types.Header, p

// Ensure that the difficulty corresponds to the turn-ness of the signer
if !p.fakeDiff {
inturn := snap.inturn(header.Number.Uint64(), signer)
inturn := snap.inturn(signer)
if inturn && header.Difficulty.Cmp(diffInTurn) != 0 {
return errWrongDifficulty
}
Expand Down Expand Up @@ -626,8 +636,7 @@ func (p *Parlia) Prepare(chain consensus.ChainReader, header *types.Header) erro
if parent == nil {
return consensus.ErrUnknownAncestor
}

header.Time = parent.Time + p.config.Period
header.Time = p.blockTimeForRamanujanFork(snap, header, parent)
if header.Time < uint64(time.Now().Unix()) {
header.Time = uint64(time.Now().Unix())
}
Expand Down Expand Up @@ -809,14 +818,7 @@ func (p *Parlia) Seal(chain consensus.ChainReader, block *types.Block, results c
}

// Sweet, the protocol permits us to sign the block, wait for our time
delay := time.Until(time.Unix(int64(header.Time), 0)) // nolint: gosimple
if header.Difficulty.Cmp(diffNoTurn) == 0 {
// It's not our turn explicitly to sign, delay it a bit
wiggle := time.Duration(len(snap.Validators)/2+1) * wiggleTime
delay += time.Duration(fixedBackOffTime) + time.Duration(rand.Int63n(int64(wiggle)))

log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))
}
delay := p.delayForRamanujanFork(snap, header)

log.Info("Sealing block with", "number", number, "delay", delay, "headerDifficulty", header.Difficulty, "val", val.Hex())

Expand Down Expand Up @@ -861,7 +863,7 @@ func (p *Parlia) CalcDifficulty(chain consensus.ChainReader, time uint64, parent
// that a new block should have based on the previous blocks in the chain and the
// current signer.
func CalcDifficulty(snap *Snapshot, signer common.Address) *big.Int {
if snap.inturn(snap.Number+1, signer) {
if snap.inturn(signer) {
return new(big.Int).Set(diffInTurn)
}
return new(big.Int).Set(diffNoTurn)
Expand Down Expand Up @@ -1140,6 +1142,26 @@ func encodeSigHeader(w io.Writer, header *types.Header, chainId *big.Int) {
}
}

func backOffTime(snap *Snapshot, val common.Address) uint64 {
if snap.inturn(val) {
return 0
} else {
dis := snap.distanceToInTurn(val)
s := rand.NewSource(int64(snap.Number))
r := rand.New(s)
n := len(snap.Validators)
backOffSteps := make([]uint64, 0, n)
for idx := uint64(0); idx < uint64(n); idx++ {
backOffSteps = append(backOffSteps, idx)
}
r.Shuffle(n, func(i, j int) {
backOffSteps[i], backOffSteps[j] = backOffSteps[j], backOffSteps[i]
})
delay := initialBackOffTime + backOffSteps[dis]*wiggleTime
return delay
}
}

// chain context
type chainContext struct {
Chain consensus.ChainReader
Expand Down Expand Up @@ -1194,4 +1216,3 @@ func applyMessage(
}
return msg.Gas() - returnGas, err
}

41 changes: 25 additions & 16 deletions consensus/parlia/parlia_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"math/rand"
"testing"
"time"

"github.com/ethereum/go-ethereum/common"
)
Expand Down Expand Up @@ -57,7 +56,7 @@ func simulateValidatorOutOfService(totalValidators int, downValidators int) {
return validators[idx]
}

downDelay := time.Duration(0)
downDelay := uint64(0)
for h := 1; h <= downBlocks; h++ {
if limit := uint64(totalValidators/2 + 1); uint64(h) >= limit {
delete(recents, uint64(h)-limit)
Expand All @@ -73,21 +72,21 @@ func simulateValidatorOutOfService(totalValidators int, downValidators int) {
if len(candidates) == 0 {
panic("can not test such case")
}
idx, delay := producerBlockDelay(candidates, totalValidators)
idx, delay := producerBlockDelay(candidates, h, totalValidators)
downDelay = downDelay + delay
recents[uint64(h)] = idx
} else {
recents[uint64(h)] = proposer
}
}
fmt.Printf("average delay is %v when there is %d validators and %d is down \n",
downDelay/time.Duration(downBlocks), totalValidators, downValidators)
downDelay/uint64(downBlocks), totalValidators, downValidators)

for i := 0; i < downValidators; i++ {
validators[down[i]] = true
}

recoverDelay := time.Duration(0)
recoverDelay := uint64(0)
lastseen := downBlocks
for h := downBlocks + 1; h <= downBlocks+recoverBlocks; h++ {
if limit := uint64(totalValidators/2 + 1); uint64(h) >= limit {
Expand All @@ -105,7 +104,7 @@ func simulateValidatorOutOfService(totalValidators int, downValidators int) {
if len(candidates) == 0 {
panic("can not test such case")
}
idx, delay := producerBlockDelay(candidates, totalValidators)
idx, delay := producerBlockDelay(candidates, h, totalValidators)
recoverDelay = recoverDelay + delay
recents[uint64(h)] = idx
} else {
Expand All @@ -116,18 +115,28 @@ func simulateValidatorOutOfService(totalValidators int, downValidators int) {
recoverDelay, downValidators, lastseen)
}

func producerBlockDelay(candidates map[int]bool, numOfValidators int) (int, time.Duration) {
minDur := time.Duration(0)
minIdx := 0
wiggle := time.Duration(numOfValidators/2+1) * wiggleTime
for idx := range candidates {
sleepTime := rand.Int63n(int64(wiggle))
if int64(minDur) < sleepTime {
minDur = time.Duration(rand.Int63n(int64(wiggle)))
minIdx = idx
func producerBlockDelay(candidates map[int]bool, height, numOfValidators int) (int, uint64) {

s := rand.NewSource(int64(height))
r := rand.New(s)
n := numOfValidators
backOffSteps := make([]int, 0, n)
for idx := 0; idx < n; idx++ {
backOffSteps = append(backOffSteps, idx)
}
r.Shuffle(n, func(i, j int) {
backOffSteps[i], backOffSteps[j] = backOffSteps[j], backOffSteps[i]
})
minDelay := numOfValidators
minCandidate := 0
for c := range candidates {
if minDelay > backOffSteps[c] {
minDelay = backOffSteps[c]
minCandidate = c
}
}
return minIdx, minDur
delay := initialBackOffTime + uint64(minDelay)*wiggleTime
return minCandidate, delay
}

func randomAddress() common.Address {
Expand Down
40 changes: 40 additions & 0 deletions consensus/parlia/ramanujanfork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package parlia

import (
"math/rand"
"time"

"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/types"
)

const (
wiggleTimeBeforeFork = 500 * time.Millisecond // Random delay (per signer) to allow concurrent signers
fixedBackOffTimeBeforeFork = 200 * time.Millisecond
)

func (p *Parlia) delayForRamanujanFork(snap *Snapshot, header *types.Header) time.Duration {
delay := time.Unix(int64(header.Time), 0).Sub(time.Now()) // nolint: gosimple
if p.chainConfig.IsRamanujan(header.Number) {
return delay
}
wiggle := time.Duration(len(snap.Validators)/2+1) * wiggleTimeBeforeFork
return delay + time.Duration(fixedBackOffTimeBeforeFork) + time.Duration(rand.Int63n(int64(wiggle)))
}

func (p *Parlia) blockTimeForRamanujanFork(snap *Snapshot, header, parent *types.Header) uint64 {
blockTime := parent.Time + p.config.Period
if p.chainConfig.IsRamanujan(header.Number) {
blockTime = blockTime + backOffTime(snap, p.val)
}
return blockTime
}

func (p *Parlia) blockTimeVerifyForRamanujanFork(snap *Snapshot, header, parent *types.Header) error {
if p.chainConfig.IsRamanujan(header.Number) {
if header.Time < parent.Time+p.config.Period+backOffTime(snap, header.Coinbase) {
return consensus.ErrFutureBlock
}
}
return nil
}
18 changes: 16 additions & 2 deletions consensus/parlia/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,26 @@ func (s *Snapshot) validators() []common.Address {
}

// inturn returns if a validator at a given block height is in-turn or not.
func (s *Snapshot) inturn(number uint64, validator common.Address) bool {
func (s *Snapshot) inturn(validator common.Address) bool {
validators := s.validators()
offset := number % uint64(len(validators))
offset := (s.Number + 1) % uint64(len(validators))
return validators[offset] == validator
}

func (s *Snapshot) distanceToInTurn(validator common.Address) uint64 {
validators := s.validators()
offset := (s.Number + 1) % uint64(len(validators))
idx := uint64(0)
for idx < uint64(len(validator)) && validators[idx] != validator {
idx++
}
if offset > idx {
return uint64(len(validators)) + idx - offset
} else {
return idx - offset
}
}

func (s *Snapshot) supposeValidator() common.Address {
validators := s.validators()
index := (s.Number + 1) % uint64(len(validators))
Expand Down
8 changes: 7 additions & 1 deletion core/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, genesis *Genesis, override
// Special case: don't change the existing config of a non-mainnet chain if no new
// config is supplied. These chains would get AllProtocolChanges (and a compat error)
// if we just continued here.
if genesis == nil && stored != params.MainnetGenesisHash {
// The full node of two BSC testnets may run without genesis file after been inited.
if genesis == nil && stored != params.MainnetGenesisHash &&
stored != params.ChapelGenesisHash && stored != params.RialtoGenesisHash {
return storedcfg, stored, nil
}

Expand Down Expand Up @@ -252,6 +254,10 @@ func (g *Genesis) configOrDefault(ghash common.Hash) *params.ChainConfig {
return params.RinkebyChainConfig
case ghash == params.GoerliGenesisHash:
return params.GoerliChainConfig
case ghash == params.ChapelGenesisHash:
return params.ChapelChainConfig
case ghash == params.RialtoGenesisHash:
return params.RialtoChainConfig
default:
return params.AllEthashProtocolChanges
}
Expand Down
5 changes: 3 additions & 2 deletions eth/fetcher/block_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,11 +681,12 @@ func (f *BlockFetcher) insert(peer string, block *types.Block) {
go f.broadcastBlock(block, true)

case consensus.ErrFutureBlock:
// Weird future block, don't fail, but neither propagate
log.Error("Received future block", "peer", peer, "number", block.Number(), "hash", hash, "err", err)
f.dropPeer(peer)

default:
// Something went very wrong, drop the peer
log.Debug("Propagated block verification failed", "peer", peer, "number", block.Number(), "hash", hash, "err", err)
log.Error("Propagated block verification failed", "peer", peer, "number", block.Number(), "hash", hash, "err", err)
f.dropPeer(peer)
return
}
Expand Down
Loading

0 comments on commit a8c9e53

Please sign in to comment.