Skip to content

Commit

Permalink
BEP-341: Validators can produce consecutive blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
MatusKysel committed Jul 24, 2024
1 parent 515eed0 commit c826e03
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 48 deletions.
13 changes: 13 additions & 0 deletions consensus/parlia/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,19 @@ const validatorSetABI = `
],
"stateMutability": "view"
},
{
"inputs": [],
"name": "getTurnLength",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"type": "function",
"name": "getValidators",
Expand Down
19 changes: 19 additions & 0 deletions consensus/parlia/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,22 @@ func (api *API) GetValidatorsAtHash(hash libcommon.Hash) ([]libcommon.Address, e
}
return snap.validators(), nil
}

func (api *API) GetTurnLength(number *rpc.BlockNumber) (uint8, error) {
// Retrieve the requested block number (or current if none requested)
var header *types.Header
if number == nil || *number == rpc.LatestBlockNumber {
header = api.chain.CurrentHeader()
} else {
header = api.chain.GetHeaderByNumber(uint64(number.Int64()))
}
// Ensure we have an actually valid block and return the validators from its snapshot
if header == nil {
return 0, errUnknownBlock
}
snap, err := api.parlia.snapshot(api.chain, header.Number.Uint64(), header.Hash(), nil, false /* verify */)
if err != nil || snap.TurnLength == 0 {
return 0, err
}
return snap.TurnLength, nil
}
74 changes: 74 additions & 0 deletions consensus/parlia/bohrFork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package parlia

import (
"errors"
"math/big"
mrand "math/rand"

"github.com/ledgerwatch/erigon/common/u256"
"github.com/ledgerwatch/erigon/consensus"
"github.com/ledgerwatch/erigon/core/state"
"github.com/ledgerwatch/erigon/core/systemcontracts"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/params"
"github.com/ledgerwatch/log/v3"
)

func (p *Parlia) getTurnLength(chain consensus.ChainHeaderReader, header *types.Header, ibs *state.IntraBlockState) (*uint8, error) {
parent := chain.GetHeaderByHash(header.ParentHash)
if parent == nil {
return nil, errors.New("parent not found")
}

var turnLength uint8
if p.chainConfig.IsBohr(parent.Number.Uint64(), parent.Time) {
turnLengthFromContract, err := p.getTurnLengthFromContract(parent, ibs)
if err != nil {
return nil, err
}
if turnLengthFromContract == nil {
return nil, errors.New("unexpected error when getTurnLengthFromContract")
}
turnLength = uint8(turnLengthFromContract.Int64())
} else {
turnLength = defaultTurnLength
}
log.Debug("getTurnLength", "turnLength", turnLength)

return &turnLength, nil
}

func (p *Parlia) getTurnLengthFromContract(header *types.Header, ibs *state.IntraBlockState) (turnLength *big.Int, err error) {
// mock to get turnLength from the contract
if params.FixedTurnLength >= 1 && params.FixedTurnLength <= 9 {
if params.FixedTurnLength == 2 {
return p.getRandTurnLength(header)
}
return big.NewInt(int64(params.FixedTurnLength)), nil
}

method := "getTurnLength"
data, err := p.validatorSetABI.Pack(method)
if err != nil {
log.Error("Unable to pack tx for getTurnLength", "error", err)
return nil, err
}

// do smart contract call
msgData := Bytes(data)
_, returnData, err := p.systemCall(header.Coinbase, systemcontracts.ValidatorContract, msgData[:], ibs, header, u256.Num0)
if err != nil {
return nil, err
}

err = p.validatorSetABI.UnpackIntoInterface(&turnLength, method, returnData)
return turnLength, err
}

// getRandTurnLength returns a random valid value, used to test switching turn length
func (p *Parlia) getRandTurnLength(header *types.Header) (turnLength *big.Int, err error) {
turnLengths := [8]uint8{1, 3, 4, 5, 6, 7, 8, 9}
r := mrand.New(mrand.NewSource(int64(header.Time)))
lengthIndex := int(r.Int31n(int32(len(turnLengths))))
return big.NewInt(int64(turnLengths[lengthIndex])), nil
}
86 changes: 77 additions & 9 deletions consensus/parlia/parlia.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ const (

CheckpointInterval = 1024 // Number of blocks after which to save the snapshot to the database
defaultEpochLength = uint64(100) // Default number of blocks of checkpoint to update validatorSet from contract
defaultTurnLength = uint8(1) // Default consecutive number of blocks a validator receives priority for block production

extraVanity = 32 // Fixed number of extra-data prefix bytes reserved for signer vanity
extraSeal = 65 // Fixed number of extra-data suffix bytes reserved for signer seal
nextForkHashSize = 4 // Fixed number of extra-data suffix bytes reserved for nextForkHash.
turnLengthSize = 1 // Fixed number of extra-data suffix bytes reserved for turnLength

validatorBytesLengthBeforeLuban = length.Addr
validatorBytesLength = length.Addr + types.BLSPublicKeyLength
Expand Down Expand Up @@ -124,6 +126,10 @@ var (
// invalid list of validators (i.e. non divisible by 20 bytes).
errInvalidSpanValidators = errors.New("invalid validator list on sprint end block")

// errInvalidTurnLength is returned if a block contains an
// invalid length of turn (i.e. no data left after parsing validators).
errInvalidTurnLength = errors.New("invalid turnLength")

// errInvalidMixDigest is returned if a block's mix digest is non-zero.
errInvalidMixDigest = errors.New("non-zero mix digest")

Expand All @@ -134,6 +140,10 @@ var (
// list of validators different than the one the local node calculated.
errMismatchingEpochValidators = errors.New("mismatching validator list on epoch block")

// errMismatchingEpochTurnLength is returned if a sprint block contains a
// turn length different than the one the local node calculated.
errMismatchingEpochTurnLength = errors.New("mismatching turn length on epoch block")

// errInvalidDifficulty is returned if the difficulty of a block is missing.
errInvalidDifficulty = errors.New("invalid difficulty")

Expand Down Expand Up @@ -335,6 +345,7 @@ func (p *Parlia) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*typ
// On Luban fork, we introduce vote attestation into the header's extra field, so extra format is different from before.
// Before Luban fork: |---Extra Vanity---|---Validators Bytes (or Empty)---|---Extra Seal---|
// After Luban fork: |---Extra Vanity---|---Validators Number and Validators Bytes (or Empty)---|---Vote Attestation (or Empty)---|---Extra Seal---|
// After bohr fork: |---Extra Vanity---|---Validators Number and Validators Bytes (or Empty)---|---Turn Length (or Empty)---|---Vote Attestation (or Empty)---|---Extra Seal---|
func getValidatorBytesFromHeader(header *types.Header, chainConfig *chain.Config, parliaConfig *chain.ParliaConfig) []byte {
if len(header.Extra) <= extraVanity+extraSeal {
return nil
Expand All @@ -351,11 +362,15 @@ func getValidatorBytesFromHeader(header *types.Header, chainConfig *chain.Config
return nil
}
num := int(header.Extra[extraVanity])
if num == 0 || len(header.Extra) <= extraVanity+extraSeal+num*validatorBytesLength {
return nil
}
start := extraVanity + validatorNumberSize
end := start + num*validatorBytesLength
extraMinLen := end + extraSeal
if chainConfig.IsBohr(header.Number.Uint64(), header.Time) {
extraMinLen += turnLengthSize
}
if num == 0 || len(header.Extra) < extraMinLen {
return nil
}
return header.Extra[start:end]
}

Expand All @@ -374,11 +389,14 @@ func getVoteAttestationFromHeader(header *types.Header, chainConfig *chain.Confi
attestationBytes = header.Extra[extraVanity : len(header.Extra)-extraSeal]
} else {
num := int(header.Extra[extraVanity])
if len(header.Extra) <= extraVanity+extraSeal+validatorNumberSize+num*validatorBytesLength {
return nil, nil
}
start := extraVanity + validatorNumberSize + num*validatorBytesLength
if chainConfig.IsBohr(header.Number.Uint64(), header.Time) {
start += turnLengthSize
}
end := len(header.Extra) - extraSeal
if end <= start {
return nil, nil
}
attestationBytes = header.Extra[start:end]
}

Expand Down Expand Up @@ -672,7 +690,7 @@ func (p *Parlia) verifySeal(chain consensus.ChainHeaderReader, header *types.Hea
return fmt.Errorf("parlia.verifySeal: headerNum=%d, validator=%x, %w", header.Number.Uint64(), signer.Bytes(), errUnauthorizedValidator)
}

if snap.signedRecently(signer) {
if snap.SignRecently(signer) {
return errRecentlySigned
}

Expand Down Expand Up @@ -723,6 +741,24 @@ func (p *Parlia) prepareValidators(header *types.Header, chain consensus.ChainHe
return nil
}

func (p *Parlia) prepareTurnLength(chain consensus.ChainHeaderReader, header *types.Header, ibs *state.IntraBlockState) error {
if header.Number.Uint64()%p.config.Epoch != 0 ||
!p.chainConfig.IsBohr(header.Number.Uint64(), header.Time) {
return nil
}

turnLength, err := p.getTurnLength(chain, header, ibs)
if err != nil {
return err
}

if turnLength != nil {
header.Extra = append(header.Extra, *turnLength)
}

return nil
}

// snapshot retrieves the authorization snapshot at a given point in time.
// !!! be careful
// the block with `number` and `hash` is just the last element of `parents`,
Expand Down Expand Up @@ -874,6 +910,10 @@ func (p *Parlia) Prepare(chain consensus.ChainHeaderReader, header *types.Header
return err
}

if err := p.prepareTurnLength(chain, header, ibs); err != nil {
return err
}

// add extra seal space
header.Extra = append(header.Extra, make([]byte, extraSeal)...)

Expand Down Expand Up @@ -927,6 +967,30 @@ func (p *Parlia) verifyValidators(header, parentHeader *types.Header, state *sta
return nil
}

func (p *Parlia) verifyTurnLength(chain consensus.ChainHeaderReader, header *types.Header, ibs *state.IntraBlockState) error {
if header.Number.Uint64()%p.config.Epoch != 0 ||
!p.chainConfig.IsBohr(header.Number.Uint64(), header.Time) {
return nil
}

turnLengthFromHeader, err := parseTurnLength(header, p.chainConfig, p.config)
if err != nil {
return err
}
if turnLengthFromHeader != nil {
turnLength, err := p.getTurnLength(chain, header, ibs)
if err != nil {
return err
}
if turnLength != nil && *turnLength == *turnLengthFromHeader {
log.Debug("verifyTurnLength", "turnLength", *turnLength)
return nil
}
}

return errMismatchingEpochTurnLength
}

// Initialize runs any pre-transaction state modifications (e.g. epoch start)
func (p *Parlia) Initialize(config *chain.Config, chain consensus.ChainHeaderReader, header *types.Header,
state *state.IntraBlockState, syscall consensus.SysCallCustom, logger log.Logger) {
Expand Down Expand Up @@ -991,6 +1055,10 @@ func (p *Parlia) finalize(header *types.Header, state *state.IntraBlockState, tx
return nil, nil, err
}

if err := p.verifyTurnLength(chain, header, state); err != nil {
return nil, nil, err
}

if p.chainConfig.IsFeynman(header.Number.Uint64(), header.Time) {
systemcontracts.UpgradeBuildInSystemContract(p.chainConfig, header.Number, parentHeader.Time, header.Time, state, logger)
}
Expand All @@ -1009,10 +1077,10 @@ func (p *Parlia) finalize(header *types.Header, state *state.IntraBlockState, tx
}
}
if header.Difficulty.Cmp(diffInTurn) != 0 {
spoiledVal := snap.supposeValidator()
spoiledVal := snap.inturnValidator()
signedRecently := false
if p.chainConfig.IsPlato(header.Number.Uint64()) {
if snap.signedRecently(spoiledVal) {
if snap.SignRecently(spoiledVal) {
signedRecently = true
}
} else {
Expand Down
Loading

0 comments on commit c826e03

Please sign in to comment.