Skip to content

Commit

Permalink
Blockchain: optional consensus (#2002)
Browse files Browse the repository at this point in the history
* blockchain: add optional consensus param

* blockchain: add algorithm property to consensus

* blockchain: rework consensus setup

* fix examples

* fix blockchain test runner

* add more tests

* add blockchain checks to clique

* skip merge check on custom consensus

* Fix consensus check and add more tests

* lint fix
  • Loading branch information
acolytec3 authored Jul 4, 2022
1 parent 3ba0078 commit 6618e55
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 54 deletions.
42 changes: 25 additions & 17 deletions packages/blockchain/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,22 @@ export class Blockchain implements BlockchainInterface {
this.db = opts.db ? opts.db : new MemoryLevel()
this.dbManager = new DBManager(this.db, this._common)

switch (this._common.consensusAlgorithm()) {
case ConsensusAlgorithm.Casper:
this.consensus = new CasperConsensus({ blockchain: this })
break
case ConsensusAlgorithm.Clique:
this.consensus = new CliqueConsensus({ blockchain: this })
break
case ConsensusAlgorithm.Ethash:
this.consensus = new EthashConsensus({ blockchain: this })
break
default:
throw new Error(`consensus algorithm ${this._common.consensusAlgorithm()} not supported`)
if (opts.consensus) {
this.consensus = opts.consensus
} else {
switch (this._common.consensusAlgorithm()) {
case ConsensusAlgorithm.Casper:
this.consensus = new CasperConsensus()
break
case ConsensusAlgorithm.Clique:
this.consensus = new CliqueConsensus()
break
case ConsensusAlgorithm.Ethash:
this.consensus = new EthashConsensus()
break
default:
throw new Error(`consensus algorithm ${this._common.consensusAlgorithm()} not supported`)
}
}

if (this._validateConsensus) {
Expand Down Expand Up @@ -184,6 +188,8 @@ export class Blockchain implements BlockchainInterface {
* @hidden
*/
private async _init(genesisBlock?: Block): Promise<void> {
await this.consensus.setup({ blockchain: this })

if (this._isInitialized) return
let dbGenesisBlock
try {
Expand Down Expand Up @@ -230,8 +236,6 @@ export class Blockchain implements BlockchainInterface {
await this.consensus.genesisInit(genesisBlock)
}

await this.consensus.setup()

// At this point, we can safely set the genesis:
// it is either the one we put in the DB, or it is equal to the one
// which we read from the DB.
Expand Down Expand Up @@ -1170,20 +1174,24 @@ export class Blockchain implements BlockchainInterface {
protected checkAndTransitionHardForkByNumber(number: bigint, td?: BigIntLike): void {
this._common.setHardforkByBlockNumber(number, td)

// If custom consensus algorithm is used, skip merge hardfork consensus checks
if (!Object.values(ConsensusAlgorithm).includes(this.consensus.algorithm as ConsensusAlgorithm))
return

switch (this._common.consensusAlgorithm()) {
case ConsensusAlgorithm.Casper:
if (!(this.consensus instanceof CasperConsensus)) {
this.consensus = new CasperConsensus({ blockchain: this })
this.consensus = new CasperConsensus()
}
break
case ConsensusAlgorithm.Clique:
if (!(this.consensus instanceof CliqueConsensus)) {
this.consensus = new CliqueConsensus({ blockchain: this })
this.consensus = new CliqueConsensus()
}
break
case ConsensusAlgorithm.Ethash:
if (!(this.consensus instanceof EthashConsensus)) {
this.consensus = new EthashConsensus({ blockchain: this })
this.consensus = new EthashConsensus()
}
break
default:
Expand Down
10 changes: 5 additions & 5 deletions packages/blockchain/src/consensus/casper.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { BlockHeader } from '@ethereumjs/block'
import Blockchain from '..'
import { Consensus, ConsensusOptions } from './interface'
import { ConsensusAlgorithm } from '@ethereumjs/common'
import { Consensus } from './interface'

/**
* This class encapsulates Casper-related consensus functionality when used with the Blockchain class.
*/
export class CasperConsensus implements Consensus {
blockchain: Blockchain
algorithm: ConsensusAlgorithm

constructor({ blockchain }: ConsensusOptions) {
this.blockchain = blockchain
constructor() {
this.algorithm = ConsensusAlgorithm.Casper
}

public async genesisInit(): Promise<void> {}
Expand Down
49 changes: 34 additions & 15 deletions packages/blockchain/src/consensus/clique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Address, bigIntToBuffer, bufferToBigInt, arrToBufArr, bufArrToArr } fro
import RLP from 'rlp'
import Blockchain from '..'
import { Consensus, ConsensusOptions } from './interface'
import { CliqueConfig } from '@ethereumjs/common'
import { CliqueConfig, ConsensusAlgorithm } from '@ethereumjs/common'

const debug = createDebugLogger('blockchain:clique')

Expand Down Expand Up @@ -46,7 +46,8 @@ type CliqueLatestBlockSigners = CliqueBlockSigner[]
* This class encapsulates Clique-related consensus functionality when used with the Blockchain class.
*/
export class CliqueConsensus implements Consensus {
blockchain: Blockchain
blockchain: Blockchain | undefined
algorithm: ConsensusAlgorithm

/**
* Keep signer history data (signer states and votes)
Expand Down Expand Up @@ -95,11 +96,18 @@ export class CliqueConsensus implements Consensus {
*/
public _cliqueLatestBlockSigners: CliqueLatestBlockSigners = []

constructor({ blockchain }: ConsensusOptions) {
this.blockchain = blockchain
constructor() {
this.algorithm = ConsensusAlgorithm.Clique
}

async setup(): Promise<void> {
/**
*
* @param param dictionary containin a {@link Blockchain} object
*
* Note: this method must be called before consensus checks are used or type errors will occur
*/
async setup({ blockchain }: ConsensusOptions): Promise<void> {
this.blockchain = blockchain
this._cliqueLatestSignerStates = await this.getCliqueLatestSignerStates()
this._cliqueLatestVotes = await this.getCliqueLatestVotes()
this._cliqueLatestBlockSigners = await this.getCliqueLatestBlockSigners()
Expand All @@ -110,6 +118,10 @@ export class CliqueConsensus implements Consensus {
}

async validateConsensus(block: Block): Promise<void> {
if (!this.blockchain) {
throw new Error('blockchain not provided')
}

const { header } = block
const valid = header.cliqueVerifySignature(this.cliqueActiveSigners())
if (!valid) {
Expand Down Expand Up @@ -137,6 +149,10 @@ export class CliqueConsensus implements Consensus {
}

async validateDifficulty(header: BlockHeader): Promise<void> {
if (!this.blockchain) {
throw new Error('blockchain not provided')
}

if (header.difficulty !== CLIQUE_DIFF_INTURN && header.difficulty !== CLIQUE_DIFF_NOTURN) {
const msg = `difficulty for clique block must be INTURN (2) or NOTURN (1), received: ${header.difficulty}`
throw new Error(`${msg} ${header.errorStr()}`)
Expand Down Expand Up @@ -168,7 +184,7 @@ export class CliqueConsensus implements Consensus {
if (commonAncestorNumber !== undefined) {
await this._cliqueDeleteSnapshots(commonAncestorNumber + BigInt(1))
for (let number = commonAncestorNumber + BigInt(1); number <= header.number; number++) {
const canonicalHeader = await this.blockchain.getCanonicalHeader(number)
const canonicalHeader = await this.blockchain!.getCanonicalHeader(number)
await this._cliqueBuildSnapshots(canonicalHeader)
}
}
Expand Down Expand Up @@ -219,7 +235,7 @@ export class CliqueConsensus implements Consensus {
bigIntToBuffer(state[0]),
state[1].map((a) => a.toBuffer()),
])
await this.blockchain.db.put(
await this.blockchain!.db.put(
CLIQUE_SIGNERS_KEY,
Buffer.from(RLP.encode(bufArrToArr(formatted))),
DB_OPTS
Expand Down Expand Up @@ -252,7 +268,7 @@ export class CliqueConsensus implements Consensus {
const lastEpochBlockNumber =
header.number -
(header.number %
BigInt((this.blockchain._common.consensusConfig() as CliqueConfig).epoch))
BigInt((this.blockchain!._common.consensusConfig() as CliqueConfig).epoch))
const limit = this.cliqueSignerLimit()
let activeSigners = this.cliqueActiveSigners()
let consensus = false
Expand Down Expand Up @@ -363,7 +379,7 @@ export class CliqueConsensus implements Consensus {
const lastEpochBlockNumber =
lastBlockNumber -
(lastBlockNumber %
BigInt((this.blockchain._common.consensusConfig() as CliqueConfig).epoch))
BigInt((this.blockchain!._common.consensusConfig() as CliqueConfig).epoch))
const blockLimit = lastEpochBlockNumber - BigInt(limit)
this._cliqueLatestVotes = this._cliqueLatestVotes.filter((state) => state[0] >= blockLimit)
}
Expand All @@ -373,7 +389,7 @@ export class CliqueConsensus implements Consensus {
bigIntToBuffer(v[0]),
[v[1][0].toBuffer(), v[1][1].toBuffer(), v[1][2]],
])
await this.blockchain.db.put(
await this.blockchain!.db.put(
CLIQUE_VOTES_KEY,
Buffer.from(RLP.encode(bufArrToArr(formatted))),
DB_OPTS
Expand Down Expand Up @@ -481,7 +497,7 @@ export class CliqueConsensus implements Consensus {
bigIntToBuffer(b[0]),
b[1].toBuffer(),
])
await this.blockchain.db.put(
await this.blockchain!.db.put(
CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY,
Buffer.from(RLP.encode(bufArrToArr(formatted))),
DB_OPTS
Expand All @@ -494,7 +510,10 @@ export class CliqueConsensus implements Consensus {
*/
private async getCliqueLatestSignerStates(): Promise<CliqueLatestSignerStates> {
try {
const signerStates = await this.blockchain.db.get<string, Buffer>(CLIQUE_SIGNERS_KEY, DB_OPTS)
const signerStates = await this.blockchain!.db.get<string, Buffer>(
CLIQUE_SIGNERS_KEY,
DB_OPTS
)
const states = arrToBufArr(RLP.decode(Uint8Array.from(signerStates))) as [Buffer, Buffer[]]
return states.map((state) => {
const blockNum = bufferToBigInt(state[0] as Buffer)
Expand All @@ -515,7 +534,7 @@ export class CliqueConsensus implements Consensus {
*/
private async getCliqueLatestVotes(): Promise<CliqueLatestVotes> {
try {
const signerVotes = await this.blockchain.db.get<string, Buffer>(CLIQUE_VOTES_KEY, DB_OPTS)
const signerVotes = await this.blockchain!.db.get<string, Buffer>(CLIQUE_VOTES_KEY, DB_OPTS)
const votes = arrToBufArr(RLP.decode(Uint8Array.from(signerVotes))) as [
Buffer,
[Buffer, Buffer, Buffer]
Expand All @@ -541,7 +560,7 @@ export class CliqueConsensus implements Consensus {
*/
private async getCliqueLatestBlockSigners(): Promise<CliqueLatestBlockSigners> {
try {
const blockSigners = await this.blockchain.db.get<string, Buffer>(
const blockSigners = await this.blockchain!.db.get<string, Buffer>(
CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY,
DB_OPTS
)
Expand Down Expand Up @@ -581,7 +600,7 @@ export class CliqueConsensus implements Consensus {
if (signerIndex === -1) {
throw new Error('Signer not found')
}
const { number } = await this.blockchain.getCanonicalHeadHeader()
const { number } = await this.blockchain!.getCanonicalHeadHeader()
//eslint-disable-next-line
return (number + BigInt(1)) % BigInt(signers.length) === BigInt(signerIndex)
}
Expand Down
22 changes: 16 additions & 6 deletions packages/blockchain/src/consensus/ethash.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Block, BlockHeader } from '@ethereumjs/block'
import { ConsensusAlgorithm } from '@ethereumjs/common'
import Ethash, { EthashCacheDB } from '@ethereumjs/ethash'
import Blockchain from '..'
import { Consensus, ConsensusOptions } from './interface'
Expand All @@ -7,15 +8,18 @@ import { Consensus, ConsensusOptions } from './interface'
* This class encapsulates Ethash-related consensus functionality when used with the Blockchain class.
*/
export class EthashConsensus implements Consensus {
blockchain: Blockchain
_ethash: Ethash
blockchain: Blockchain | undefined
algorithm: ConsensusAlgorithm
_ethash: Ethash | undefined

constructor({ blockchain }: ConsensusOptions) {
this.blockchain = blockchain
this._ethash = new Ethash(this.blockchain.db as unknown as EthashCacheDB)
constructor() {
this.algorithm = ConsensusAlgorithm.Ethash
}

async validateConsensus(block: Block): Promise<void> {
if (!this._ethash) {
throw new Error('blockchain not provided')
}
const valid = await this._ethash.verifyPOW(block)
if (!valid) {
throw new Error('invalid POW')
Expand All @@ -27,13 +31,19 @@ export class EthashConsensus implements Consensus {
* @param header - header of block to be checked
*/
async validateDifficulty(header: BlockHeader) {
if (!this.blockchain) {
throw new Error('blockchain not provided')
}
const parentHeader = (await this.blockchain.getBlock(header.parentHash)).header
if (header.ethashCanonicalDifficulty(parentHeader) !== header.difficulty) {
throw new Error(`invalid difficulty ${header.errorStr()}`)
}
}

public async genesisInit(): Promise<void> {}
public async setup(): Promise<void> {}
public async setup({ blockchain }: ConsensusOptions): Promise<void> {
this.blockchain = blockchain
this._ethash = new Ethash(this.blockchain.db as unknown as EthashCacheDB)
}
public async newBlock(): Promise<void> {}
}
4 changes: 3 additions & 1 deletion packages/blockchain/src/consensus/interface.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Block, BlockHeader } from '@ethereumjs/block'
import { ConsensusAlgorithm } from '@ethereumjs/common'
import Blockchain from '..'

/**
* Interface that a consensus class needs to implement.
*/
export interface Consensus {
algorithm: ConsensusAlgorithm | string
/**
* Initialize genesis for consensus mechanism
* @param genesisBlock genesis block
Expand All @@ -14,7 +16,7 @@ export interface Consensus {
/**
* Set up consensus mechanism
*/
setup(): Promise<void>
setup({ blockchain }: ConsensusOptions): Promise<void>

/**
* Validate block consensus parameters
Expand Down
6 changes: 6 additions & 0 deletions packages/blockchain/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Block } from '@ethereumjs/block'
import Common from '@ethereumjs/common'
import { AbstractLevel } from 'abstract-level'
import { Consensus } from '.'
import { GenesisState } from './genesisStates'

export type OnBlock = (block: Block, reorg: boolean) => Promise<void> | void
Expand Down Expand Up @@ -124,4 +125,9 @@ export interface BlockchainOptions {
* ```
*/
genesisState?: GenesisState

/**
* Optional custom consensus that implements the {@link Consensus} class
*/
consensus?: Consensus
}
Loading

0 comments on commit 6618e55

Please sign in to comment.