From 8c81c6978c640f49f43942f021e56ff63b07f555 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 27 Jan 2021 22:46:18 -0800 Subject: [PATCH] clique: add reorg logic and tests --- packages/blockchain/src/index.ts | 92 +++++++++++----- packages/blockchain/test/reorg.spec.ts | 143 ++++++++++++++++++++++++- 2 files changed, 206 insertions(+), 29 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 18c9db3541..bb6b378084 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -146,7 +146,7 @@ export default class Blockchain implements BlockchainInterface { * [ [BLOCK_NUMBER_1, [SIGNER1, SIGNER 2,]], [BLOCK_NUMBER2, [SIGNER1, SIGNER3]], ...] * * The top element from the array represents the list of current signers. - * On reorgs delete the top elements from the array until BLOCK_NUMBER > REORG_BLOCK + * On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK. * * Always keep at least one item on the stack */ @@ -164,7 +164,7 @@ export default class Blockchain implements BlockchainInterface { * (nevertheless keep entries with blocks before EPOCH_BLOCK in case a reorg happens * during an epoch change) * - * On reorgs delete the top elements from the array until BLOCK_NUMBER > REORG_BLOCK. + * On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK. */ private _cliqueLatestVotes: CliqueLatestVotes = [] @@ -173,7 +173,7 @@ export default class Blockchain implements BlockchainInterface { * Kept as a snapshot for quickly checking for "recently signed" error. * Format: [ [BLOCK_NUMBER, SIGNER_ADDRESS], ...] * - * On reorgs delete the top elements from the array until BLOCK_NUMBER > REORG_BLOCK. + * On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK. */ private _cliqueLatestBlockSigners: CliqueLatestBlockSigners = [] @@ -308,14 +308,14 @@ export default class Blockchain implements BlockchainInterface { DBSetBlockOrHeader(genesisBlock).map((op) => dbOps.push(op)) DBSaveLookups(genesisHash, new BN(0)).map((op) => dbOps.push(op)) + await this.dbManager.batch(dbOps) + if (this._common.consensusAlgorithm() === 'clique') { await this.cliqueSaveGenesisSigners(genesisBlock) } - - await this.dbManager.batch(dbOps) } - // Clique: read current signer list + // Clique: read current signer states, signer votes, and block signers if (this._common.consensusAlgorithm() === 'clique') { this._cliqueLatestSignerStates = await this.dbManager.getCliqueLatestSignerStates() this._cliqueLatestVotes = await this.dbManager.getCliqueLatestVotes() @@ -424,24 +424,36 @@ export default class Blockchain implements BlockchainInterface { */ private async cliqueSaveGenesisSigners(genesisBlock: Block) { const genesisSignerState: CliqueSignerState = [ - genesisBlock.header.number, + new BN(0), genesisBlock.header.cliqueEpochTransitionSigners(), ] await this.cliqueUpdateSignerStates(genesisSignerState) await this.cliqueUpdateVotes() } - private async cliqueUpdateSignerStates(signerState: CliqueSignerState) { + /** + * Save signer state to db + * @param signerState + * @hidden + */ + private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) { const dbOps: DBOp[] = [] - this._cliqueLatestSignerStates.push(signerState) - // trim length to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT - const length = this._cliqueLatestSignerStates.length - const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT - if (length > limit) { - this._cliqueLatestSignerStates = this._cliqueLatestSignerStates.slice(length - limit, length) + if (signerState) { + this._cliqueLatestSignerStates.push(signerState) + + // trim length to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT + const length = this._cliqueLatestSignerStates.length + const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT + if (length > limit) { + this._cliqueLatestSignerStates = this._cliqueLatestSignerStates.slice( + length - limit, + length + ) + } } + // save to db const formatted = this._cliqueLatestSignerStates.map((state) => [ state[0].toBuffer(), state[1].map((a) => a.toBuffer()), @@ -543,7 +555,7 @@ export default class Blockchain implements BlockchainInterface { const dbOps: DBOp[] = [] const formatted = this._cliqueLatestVotes.map((v) => [ v[0].toBuffer(), - [v[1][0].toBuffer(), v[1][0].toBuffer(), v[1][2]], + [v[1][0].toBuffer(), v[1][1].toBuffer(), v[1][2]], ]) dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(formatted))) @@ -556,21 +568,27 @@ export default class Blockchain implements BlockchainInterface { * @param header BlockHeader * @hidden */ - private async cliqueUpdateLatestBlockSigners(header: BlockHeader) { - if (header.isGenesis()) { - return - } + private async cliqueUpdateLatestBlockSigners(header?: BlockHeader) { const dbOps: DBOp[] = [] - // add this block's signer - const signer: CliqueBlockSigner = [header.number, header.cliqueSigner()] - this._cliqueLatestBlockSigners.push(signer) + if (header) { + if (header.isGenesis()) { + return + } - // trim length to `this.cliqueSignerLimit()` - const length = this._cliqueLatestBlockSigners.length - const limit = this.cliqueSignerLimit() - if (length > limit) { - this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.slice(length - limit, length) + // add this block's signer + const signer: CliqueBlockSigner = [header.number, header.cliqueSigner()] + this._cliqueLatestBlockSigners.push(signer) + + // trim length to `this.cliqueSignerLimit()` + const length = this._cliqueLatestBlockSigners.length + const limit = this.cliqueSignerLimit() + if (length > limit) { + this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.slice( + length - limit, + length + ) + } } // save to db @@ -586,7 +604,8 @@ export default class Blockchain implements BlockchainInterface { */ public cliqueActiveSigners(): Address[] { this._requireClique() - return this._cliqueLatestSignerStates[this._cliqueLatestSignerStates.length - 1][1] + const signers = this._cliqueLatestSignerStates + return [...signers[signers.length - 1][1]] } /** @@ -1169,6 +1188,23 @@ export default class Blockchain implements BlockchainInterface { this._headBlockHash = headHash } + if (this._common.consensusAlgorithm() === 'clique') { + // remove blockNumber from clique snapshots + // (latest signer states, latest votes, latest block signers) + this._cliqueLatestSignerStates = this._cliqueLatestSignerStates.filter( + (s) => !s[0].eq(blockNumber) + ) + await this.cliqueUpdateSignerStates() + + this._cliqueLatestVotes = this._cliqueLatestVotes.filter((v) => !v[0].eq(blockNumber)) + await this.cliqueUpdateVotes() + + this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.filter( + (s) => !s[0].eq(blockNumber) + ) + await this.cliqueUpdateLatestBlockSigners() + } + blockNumber.iaddn(1) hash = await this.safeNumberToHash(blockNumber) diff --git a/packages/blockchain/test/reorg.spec.ts b/packages/blockchain/test/reorg.spec.ts index 58f9edafb2..ca38247bf0 100644 --- a/packages/blockchain/test/reorg.spec.ts +++ b/packages/blockchain/test/reorg.spec.ts @@ -1,8 +1,9 @@ import Common from '@ethereumjs/common' import { Block } from '@ethereumjs/block' -import { BN } from 'ethereumjs-util' +import { Address, BN } from 'ethereumjs-util' import tape from 'tape' import Blockchain from '../src' +import { CLIQUE_NONCE_AUTH } from '../src/clique' import { generateConsecutiveBlock } from './util' const genesis = Block.fromBlockData({ @@ -85,4 +86,144 @@ tape('reorg tests', (t) => { st.end() } ) + + t.test( + 'should correctly reorg a poa chain and remove blocks from clique snapshots', + async (st) => { + const common = new Common({ chain: 'goerli', hardfork: 'chainstart' }) + const genesisBlock = Block.genesis({}, { common }) + const blockchain = new Blockchain({ + validateBlocks: false, + validateConsensus: false, + common, + genesisBlock, + }) + + const extraData = Buffer.from( + '506172697479205465636820417574686f7269747900000000000000000000002bbf886181970654ed46e3fae0ded41ee53fec702c47431988a7ae80e6576f3552684f069af80ba11d36327aaf846d470526e4a1c461601b2fd4ebdcdc2b734a01', + 'hex' + ) // from goerli block 1 + const { gasLimit } = genesisBlock.header + const base = { extraData, gasLimit } + + const nonce = CLIQUE_NONCE_AUTH + const beneficiary1 = new Address(Buffer.alloc(20).fill(1)) + const beneficiary2 = new Address(Buffer.alloc(20).fill(2)) + + const block1_low = Block.fromBlockData( + { + header: { + ...base, + number: 1, + parentHash: genesisBlock.hash(), + timestamp: genesisBlock.header.timestamp.addn(30), + }, + }, + { common } + ) + const block2_low = Block.fromBlockData( + { + header: { + ...base, + number: 2, + parentHash: block1_low.hash(), + timestamp: block1_low.header.timestamp.addn(30), + nonce, + coinbase: beneficiary1, + }, + }, + { common } + ) + + const block1_high = Block.fromBlockData( + { + header: { + ...base, + number: 1, + parentHash: genesisBlock.hash(), + timestamp: genesisBlock.header.timestamp.addn(15), + }, + }, + { common } + ) + const block2_high = Block.fromBlockData( + { + header: { + ...base, + number: 2, + parentHash: block1_high.hash(), + timestamp: block1_high.header.timestamp.addn(15), + nonce, + coinbase: beneficiary2, + }, + }, + { common } + ) + + await blockchain.putBlocks([block1_low, block2_low]) + const head_low = await blockchain.getHead() + + await blockchain.putBlocks([block1_high, block2_high]) + const head_high = await blockchain.getHead() + + t.ok( + head_low.hash().equals(block2_low.hash()), + 'head on the low chain should equal the low block' + ) + t.ok( + head_high.hash().equals(block2_high.hash()), + 'head on the high chain should equal the high block' + ) + + let signerStates = (blockchain as any)._cliqueLatestSignerStates + t.ok( + !signerStates.find( + (s: any) => s[0].eqn(2) && s[1][1].toBuffer().equals(beneficiary1.toBuffer()) + ), + 'should not find reorged signer state' + ) + + let signerVotes = (blockchain as any)._cliqueLatestVotes + t.ok( + !signerVotes.find( + (v: any) => + v[0].eqn(2) && + v[1][0].toBuffer().equals(block1_low.header.cliqueSigner().toBuffer()) && + v[1][1].toBuffer().equals(beneficiary1.toBuffer()) && + v[1][2].equals(CLIQUE_NONCE_AUTH) + ), + 'should not find reorged clique vote' + ) + + let blockSigners = (blockchain as any)._cliqueLatestBlockSigners + t.ok( + !blockSigners.find( + (s: any) => + s[0].eqn(1) && s[1].toBuffer().equals(block1_low.header.cliqueSigner().toBuffer()) + ), + 'should not find reorged block signer' + ) + + signerStates = (blockchain as any)._cliqueLatestSignerStates + t.ok( + !!signerStates.find( + (s: any) => s[0].eqn(2) && s[1][1].toBuffer().equals(beneficiary2.toBuffer()) + ), + 'should find reorged signer state' + ) + + signerVotes = (blockchain as any)._cliqueLatestVotes + t.ok(signerVotes.length == 0, 'votes should be empty') + + blockSigners = (blockchain as any)._cliqueLatestBlockSigners + t.ok( + !!blockSigners.find( + (s: any) => + s[0].eqn(2) && s[1].toBuffer().equals(block2_high.header.cliqueSigner().toBuffer()) + ), + 'should find reorged block signer' + ) + st.end() + } + ) })