Skip to content

Commit

Permalink
Merge pull request #1032 from ethereumjs/add-clique-support
Browse files Browse the repository at this point in the history
Add Clique Support
  • Loading branch information
holgerd77 authored Jan 28, 2021
2 parents 01243f0 + 6721321 commit 28ea476
Show file tree
Hide file tree
Showing 38 changed files with 2,556 additions and 558 deletions.
837 changes: 442 additions & 395 deletions package-lock.json

Large diffs are not rendered by default.

41 changes: 40 additions & 1 deletion packages/block/src/block.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-dupe-class-members */

import { BaseTrie as Trie } from '@ethereumjs/trie'
import { BN, rlp, keccak256, KECCAK256_RLP } from 'ethereumjs-util'
import { Address, BN, rlp, keccak256, KECCAK256_RLP } from 'ethereumjs-util'
import Common from '@ethereumjs/common'
import { Transaction, TxOptions } from '@ethereumjs/tx'
import { BlockHeader } from './header'
Expand Down Expand Up @@ -105,6 +105,9 @@ export class Block {
this.transactions = transactions
this.uncleHeaders = uncleHeaders
this._common = this.header._common
if (this._common.consensusType() === 'poa' && uncleHeaders.length > 0) {
throw new Error('Block initialization with uncleHeaders on a PoA network is not allowed')
}

const freeze = opts?.freeze ?? true
if (freeze) {
Expand Down Expand Up @@ -137,6 +140,42 @@ export class Block {
return this.header.isGenesis()
}

/**
* Checks if the block is an epoch transition
* block (only clique PoA, throws otherwise)
*/
cliqueIsEpochTransition(): boolean {
return this.header.cliqueIsEpochTransition()
}

/**
* Returns extra vanity data
* (only clique PoA, throws otherwise)
*/
cliqueExtraVanity(): Buffer {
return this.header.cliqueExtraVanity()
}

/**
* Returns extra seal data
* (only clique PoA, throws otherwise)
*/
cliqueExtraSeal(): Buffer {
return this.header.cliqueExtraSeal()
}

/**
* Returns a list of signers
* (only clique PoA, throws otherwise)
*
* This function throws if not called on an epoch
* transition block and should therefore be used
* in conjunction with `cliqueIsEpochTransition()`
*/
cliqueEpochTransitionSigners(): Address[] {
return this.header.cliqueEpochTransitionSigners()
}

/**
* Returns the rlp encoding of the block.
*/
Expand Down
11 changes: 11 additions & 0 deletions packages/block/src/clique.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BN } from 'ethereumjs-util'

// Fixed number of extra-data prefix bytes reserved for signer vanity
export const CLIQUE_EXTRA_VANITY = 32
// Fixed number of extra-data suffix bytes reserved for signer seal
export const CLIQUE_EXTRA_SEAL = 65

// Block difficulty for in-turn signatures
export const CLIQUE_DIFF_INTURN = new BN(2)
// Block difficulty for in-turn signatures
export const CLIQUE_DIFF_NOTURN = new BN(1)
2 changes: 1 addition & 1 deletion packages/block/src/from-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ export default function blockFromRpc(blockParams: any, uncles: any[] = [], optio

const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options))

return Block.fromBlockData({ header, transactions, uncleHeaders })
return Block.fromBlockData({ header, transactions, uncleHeaders }, options)
}
209 changes: 194 additions & 15 deletions packages/block/src/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ import {
Address,
BN,
bnToHex,
ecrecover,
KECCAK256_RLP_ARRAY,
KECCAK256_RLP,
rlp,
rlphash,
toBuffer,
unpadBuffer,
zeros,
bufferToInt,
} from 'ethereumjs-util'
import { HeaderData, JsonHeader, BlockHeaderBuffer, Blockchain, BlockOptions } from './types'
import {
CLIQUE_EXTRA_VANITY,
CLIQUE_EXTRA_SEAL,
CLIQUE_DIFF_INTURN,
CLIQUE_DIFF_NOTURN,
} from './clique'

const DEFAULT_GAS_LIMIT = new BN(Buffer.from('ffffffffffffff', 'hex'))

Expand Down Expand Up @@ -220,7 +228,7 @@ export class BlockHeader {
this.mixHash = mixHash
this.nonce = nonce

this._validateBufferLengths()
this._validateHeaderFields()
this._checkDAOExtraData()

// Now we have set all the values of this Header, we possibly have set a dummy
Expand All @@ -239,7 +247,7 @@ export class BlockHeader {
/**
* Validates correct buffer lengths, throws if invalid.
*/
_validateBufferLengths() {
_validateHeaderFields() {
const { parentHash, stateRoot, transactionsTrie, receiptTrie, mixHash, nonce } = this
if (parentHash.length !== 32) {
throw new Error(`parentHash must be 32 bytes, received ${parentHash.length} bytes`)
Expand All @@ -258,6 +266,7 @@ export class BlockHeader {
if (mixHash.length !== 32) {
throw new Error(`mixHash must be 32 bytes, received ${mixHash.length} bytes`)
}

if (nonce.length !== 8) {
throw new Error(`nonce must be 8 bytes, received ${nonce.length} bytes`)
}
Expand Down Expand Up @@ -357,12 +366,36 @@ export class BlockHeader {
* @param parentBlockHeader - the header from the parent `Block` of this header
*/
validateDifficulty(parentBlockHeader: BlockHeader): boolean {
if (this._common.consensusType() !== 'pow') {
throw new Error('difficulty validation is currently only supported on PoW chains')
}
return this.canonicalDifficulty(parentBlockHeader).eq(this.difficulty)
}

/**
* For poa, validates `difficulty` is correctly identified as INTURN or NOTURN.
*/
validateCliqueDifficulty(blockchain: Blockchain): boolean {
if (!this.difficulty.eq(CLIQUE_DIFF_INTURN) && !this.difficulty.eq(CLIQUE_DIFF_NOTURN)) {
throw new Error(
`difficulty for clique block must be INTURN (2) or NOTURN (1), received: ${this.difficulty.toString()}`
)
}
const signers = blockchain.cliqueActiveSigners()
if (signers.length === 0) {
// abort if signers are unavailable
return true
}
const signerIndex = signers.findIndex((address: Address) =>
address.toBuffer().equals(this.cliqueSigner().toBuffer())
)
const inTurn = this.number.modn(signers.length) === signerIndex
if (
(inTurn && this.difficulty.eq(CLIQUE_DIFF_INTURN)) ||
(!inTurn && this.difficulty.eq(CLIQUE_DIFF_NOTURN))
) {
return true
}
return false
}

/**
* Validates if the block gasLimit remains in the
* boundaries set by the protocol.
Expand Down Expand Up @@ -393,7 +426,8 @@ export class BlockHeader {
* - The `parentHash` is part of the blockchain (it is a valid header)
* - Current block number is parent block number + 1
* - Current block has a strictly higher timestamp
* - Current block has valid difficulty and gas limit
* - Additional PoA -> Clique check: Current block has a timestamp diff greater or equal to PERIOD
* - Current block has valid difficulty (only PoW, otherwise pass) and gas limit
* - In case that the header is an uncle header, it should not be too old or young in the chain.
* @param blockchain - validate against a @ethereumjs/blockchain
* @param height - If this is an uncle header, this is the height of the block that is including it
Expand All @@ -403,37 +437,79 @@ export class BlockHeader {
return
}
const hardfork = this._getHardfork()
if (this.extraData.length > this._common.paramByHardfork('vm', 'maxExtraDataSize', hardfork)) {
throw new Error('invalid amount of extra data')
if (this._common.consensusAlgorithm() !== 'clique') {
if (
this.extraData.length > this._common.paramByHardfork('vm', 'maxExtraDataSize', hardfork)
) {
throw new Error('invalid amount of extra data')
}
} else {
const minLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL
if (!this.cliqueIsEpochTransition()) {
// ExtraData length on epoch transition
if (this.extraData.length !== minLength) {
throw new Error(
`extraData must be ${minLength} bytes on non-epoch transition blocks, received ${this.extraData.length} bytes`
)
}
} else {
const signerLength = this.extraData.length - minLength
if (signerLength % 20 !== 0) {
throw new Error(
`invalid signer list length in extraData, received signer length of ${signerLength} (not divisible by 20)`
)
}
// coinbase (beneficiary) on epoch transition
if (!this.coinbase.isZero()) {
throw new Error(
`coinbase must be filled with zeros on epoch transition blocks, received ${this.coinbase.toString()}`
)
}
}
// MixHash format
if (!this.mixHash.equals(Buffer.alloc(32))) {
throw new Error(`mixHash must be filled with zeros, received ${this.mixHash}`)
}
if (!this.validateCliqueDifficulty(blockchain)) {
throw new Error('invalid clique difficulty')
}
}

const header = await this._getHeaderByHash(blockchain, this.parentHash)
const parentHeader = await this._getHeaderByHash(blockchain, this.parentHash)

if (!header) {
if (!parentHeader) {
throw new Error('could not find parent header')
}

const { number } = this
if (!number.eq(header.number.addn(1))) {
if (!number.eq(parentHeader.number.addn(1))) {
throw new Error('invalid number')
}

if (this.timestamp.lte(header.timestamp)) {
if (this.timestamp.lte(parentHeader.timestamp)) {
throw new Error('invalid timestamp')
}

if (this._common.consensusAlgorithm() === 'clique') {
const period = this._common.consensusConfig().period
// Timestamp diff between blocks is lower than PERIOD (clique)
if (parentHeader.timestamp.addn(period).gt(this.timestamp)) {
throw new Error('invalid timestamp diff (lower than period)')
}
}

if (this._common.consensusType() === 'pow') {
if (!this.validateDifficulty(header)) {
if (!this.validateDifficulty(parentHeader)) {
throw new Error('invalid difficulty')
}
}

if (!this.validateGasLimit(header)) {
if (!this.validateGasLimit(parentHeader)) {
throw new Error('invalid gas limit')
}

if (height) {
const dif = height.sub(header.number)
const dif = height.sub(parentHeader.number)
if (!(dif.ltn(8) && dif.gtn(1))) {
throw new Error('uncle block has a parent that is too old or too young')
}
Expand Down Expand Up @@ -467,6 +543,9 @@ export class BlockHeader {
* Returns the hash of the block header.
*/
hash(): Buffer {
if (this._common.consensusAlgorithm() === 'clique' && !this.isGenesis()) {
return this.cliqueHash()
}
return rlphash(this.raw())
}

Expand All @@ -477,6 +556,106 @@ export class BlockHeader {
return this.number.isZero()
}

private _requireClique(name: string) {
if (this._common.consensusAlgorithm() !== 'clique') {
throw new Error(`BlockHeader.${name}() call only supported for clique PoA networks`)
}
}

/**
* Hash for PoA clique blocks is created without the seal.
* @hidden
*/
private cliqueHash() {
const raw = this.raw()
raw[12] = this.extraData.slice(0, this.extraData.length - CLIQUE_EXTRA_SEAL)
return rlphash(raw)
}

/**
* Checks if the block header is an epoch transition
* header (only clique PoA, throws otherwise)
*/
cliqueIsEpochTransition(): boolean {
this._requireClique('cliqueIsEpochTransition')
const epoch = new BN(this._common.consensusConfig().epoch)
// Epoch transition block if the block number has no
// remainder on the division by the epoch length
return this.number.mod(epoch).isZero()
}

/**
* Returns extra vanity data
* (only clique PoA, throws otherwise)
*/
cliqueExtraVanity(): Buffer {
this._requireClique('cliqueExtraVanity')
return this.extraData.slice(0, CLIQUE_EXTRA_VANITY)
}

/**
* Returns extra seal data
* (only clique PoA, throws otherwise)
*/
cliqueExtraSeal(): Buffer {
this._requireClique('cliqueExtraSeal')
return this.extraData.slice(-CLIQUE_EXTRA_SEAL)
}

/**
* Returns a list of signers
* (only clique PoA, throws otherwise)
*
* This function throws if not called on an epoch
* transition block and should therefore be used
* in conjunction with `cliqueIsEpochTransition()`
*/
cliqueEpochTransitionSigners(): Address[] {
this._requireClique('cliqueEpochTransitionSigners')
if (!this.cliqueIsEpochTransition()) {
throw new Error('Signers are only included in epoch transition blocks (clique)')
}

const start = CLIQUE_EXTRA_VANITY
const end = this.extraData.length - CLIQUE_EXTRA_SEAL
const signerBuffer = this.extraData.slice(start, end)

const signerList: Buffer[] = []
const signerLength = 20
for (let start = 0; start <= signerBuffer.length - signerLength; start += signerLength) {
signerList.push(signerBuffer.slice(start, start + signerLength))
}
return signerList.map((buf) => new Address(buf))
}

/**
* Verifies the signature of the block (last 65 bytes of extraData field)
* (only clique PoA, throws otherwise)
*
* Method throws if signature is invalid
*/
cliqueVerifySignature(signerList: Address[]): boolean {
this._requireClique('cliqueVerifySignature')
const signerAddress = this.cliqueSigner().toBuffer()
const signerFound = signerList.find((signer) => {
return signer.toBuffer().equals(signerAddress)
})
return !!signerFound
}

/**
* Returns the signer address
*/
cliqueSigner(): Address {
this._requireClique('cliqueSigner')
const extraSeal = this.cliqueExtraSeal()
const r = extraSeal.slice(0, 32)
const s = extraSeal.slice(32, 64)
const v = bufferToInt(extraSeal.slice(64, 65)) + 27
const pubKey = ecrecover(this.hash(), v, r, s)
return Address.fromPublicKey(pubKey)
}

/**
* Returns the rlp encoding of the block header.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/block/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddressLike, BNLike, BufferLike } from 'ethereumjs-util'
import { Address, AddressLike, BNLike, BufferLike } from 'ethereumjs-util'
import Common from '@ethereumjs/common'
import { TxData, JsonTx } from '@ethereumjs/tx'
import { Block } from './block'
Expand Down Expand Up @@ -130,4 +130,5 @@ export interface JsonHeader {

export interface Blockchain {
getBlock(hash: Buffer): Promise<Block>
cliqueActiveSigners(): Address[]
}
Loading

0 comments on commit 28ea476

Please sign in to comment.