From 2bf1292ef30f5a3a36a91ce007385077527b648a Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 11 Apr 2023 00:16:02 +0200 Subject: [PATCH] Clique: get signers by block num (#2610) * blockchain/clique: add support for clique signers by block num * blockchain: clique sort clique latest signer states * client: fix build * blockchain/clique: fix tests and add test * blockchain/clique: fix clique bugs * blockchain/tests/clique: fix tests * blockchain: clique: add reorg test * blockchain/clique: add reorg over epoch test (fails) [no ci] * replace equalsBytes with deepEquals * blockchain/clique: address comments * blockchain/clique: ensure tests are ran --------- Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com> --- packages/blockchain/src/consensus/clique.ts | 61 +- packages/blockchain/test/clique.spec.ts | 670 +++++++++++++------- packages/client/lib/miner/miner.ts | 13 +- 3 files changed, 509 insertions(+), 235 deletions(-) diff --git a/packages/blockchain/src/consensus/clique.ts b/packages/blockchain/src/consensus/clique.ts index b28760f788..b89aa64fc4 100644 --- a/packages/blockchain/src/consensus/clique.ts +++ b/packages/blockchain/src/consensus/clique.ts @@ -47,6 +47,10 @@ type CliqueLatestBlockSigners = CliqueBlockSigner[] /** * This class encapsulates Clique-related consensus functionality when used with the Blockchain class. + * Note: reorgs which happen between epoch transitions, which change the internal voting state over the reorg + * will result in failure and is currently not supported. + * The hotfix for this could be: re-load the latest epoch block (this has the clique state in the extraData of the header) + * Now replay all blocks on top of it. This should validate the chain up to the new/reorged tip which previously threw. */ export class CliqueConsensus implements Consensus { blockchain: Blockchain | undefined @@ -58,7 +62,7 @@ export class CliqueConsensus implements Consensus { * * This defines a limit for reorgs on PoA clique chains. */ - private CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 100 + private CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 200 /** * List with the latest signer states checkpointed on blocks where @@ -112,6 +116,7 @@ export class CliqueConsensus implements Consensus { async setup({ blockchain }: ConsensusOptions): Promise { this.blockchain = blockchain this._cliqueLatestSignerStates = await this.getCliqueLatestSignerStates() + this._cliqueLatestSignerStates.sort((a, b) => (a[0] > b[0] ? 1 : -1)) this._cliqueLatestVotes = await this.getCliqueLatestVotes() this._cliqueLatestBlockSigners = await this.getCliqueLatestBlockSigners() } @@ -126,7 +131,7 @@ export class CliqueConsensus implements Consensus { } const { header } = block - const valid = header.cliqueVerifySignature(this.cliqueActiveSigners()) + const valid = header.cliqueVerifySignature(this.cliqueActiveSigners(header.number)) if (!valid) { throw new Error('invalid PoA block signature (clique)') } @@ -140,7 +145,7 @@ export class CliqueConsensus implements Consensus { // only active (non-stale) votes will counted (if vote.blockNumber >= lastEpochBlockNumber const checkpointSigners = header.cliqueEpochTransitionSigners() - const activeSigners = this.cliqueActiveSigners() + const activeSigners = this.cliqueActiveSigners(header.number) for (const [i, cSigner] of checkpointSigners.entries()) { if (activeSigners[i]?.equals(cSigner) !== true) { throw new Error( @@ -161,7 +166,7 @@ export class CliqueConsensus implements Consensus { throw new Error(`${msg} ${header.errorStr()}`) } - const signers = this.cliqueActiveSigners() + const signers = this.cliqueActiveSigners(header.number) if (signers.length === 0) { // abort if signers are unavailable const msg = 'no signers available' @@ -215,7 +220,17 @@ export class CliqueConsensus implements Consensus { */ private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) { if (signerState) { + const blockNumber = signerState[0] + const known = this._cliqueLatestSignerStates.find((value) => { + if (value[0] === blockNumber) { + return true + } + }) + if (known !== undefined) { + return + } this._cliqueLatestSignerStates.push(signerState) + this._cliqueLatestSignerStates.sort((a, b) => (a[0] > b[0] ? 1 : -1)) } // trim to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT @@ -240,10 +255,15 @@ export class CliqueConsensus implements Consensus { ]) await this.blockchain!.db.put(CLIQUE_SIGNERS_KEY, RLP.encode(formatted), DB_OPTS) // Output active signers for debugging purposes - let i = 0 - for (const signer of this.cliqueActiveSigners()) { - debug(`Clique signer [${i}]: ${signer}`) - i++ + if (signerState !== undefined) { + let i = 0 + try { + for (const signer of this.cliqueActiveSigners(signerState[0])) { + debug(`Clique signer [${i}]: ${signer} (block: ${signerState[0]})`) + i++ + } + // eslint-disable-next-line no-empty + } catch (e) {} } } @@ -268,8 +288,8 @@ export class CliqueConsensus implements Consensus { header.number - (header.number % BigInt((this.blockchain!._common.consensusConfig() as CliqueConfig).epoch)) - const limit = this.cliqueSignerLimit() - let activeSigners = this.cliqueActiveSigners() + const limit = this.cliqueSignerLimit(header.number) + let activeSigners = [...this.cliqueActiveSigners(header.number)] let consensus = false // AUTH vote analysis @@ -400,12 +420,17 @@ export class CliqueConsensus implements Consensus { /** * Returns a list with the current block signers */ - cliqueActiveSigners(): Address[] { + cliqueActiveSigners(blockNum: bigint): Address[] { const signers = this._cliqueLatestSignerStates if (signers.length === 0) { return [] } - return [...signers[signers.length - 1][1]] + for (let i = signers.length - 1; i >= 0; i--) { + if (signers[i][0] < blockNum) { + return signers[i][1] + } + } + throw new Error(`Could not load signers for block ${blockNum}`) } /** @@ -415,8 +440,8 @@ export class CliqueConsensus implements Consensus { * 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3, ... * @hidden */ - private cliqueSignerLimit() { - return Math.floor(this.cliqueActiveSigners().length / 2) + 1 + private cliqueSignerLimit(blockNum: bigint) { + return Math.floor(this.cliqueActiveSigners(blockNum).length / 2) + 1 } /** @@ -430,7 +455,7 @@ export class CliqueConsensus implements Consensus { // skip genesis, first block return false } - const limit = this.cliqueSignerLimit() + const limit = this.cliqueSignerLimit(header.number) // construct recent block signers list with this block let signers = this._cliqueLatestBlockSigners signers = signers.slice(signers.length < limit ? 0 : 1) @@ -484,7 +509,7 @@ export class CliqueConsensus implements Consensus { // trim length to `this.cliqueSignerLimit()` const length = this._cliqueLatestBlockSigners.length - const limit = this.cliqueSignerLimit() + const limit = this.cliqueSignerLimit(header.number) if (length > limit) { this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.slice( length - limit, @@ -591,8 +616,8 @@ export class CliqueConsensus implements Consensus { * Helper to determine if a signer is in or out of turn for the next block. * @param signer The signer address */ - async cliqueSignerInTurn(signer: Address): Promise { - const signers = this.cliqueActiveSigners() + async cliqueSignerInTurn(signer: Address, blockNum: bigint): Promise { + const signers = this.cliqueActiveSigners(blockNum) const signerIndex = signers.findIndex((address) => address.equals(signer)) if (signerIndex === -1) { throw new Error('Signer not found') diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index 4cd8801f93..976a3366d7 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -10,161 +10,192 @@ import { CLIQUE_NONCE_AUTH, CLIQUE_NONCE_DROP } from '../src/consensus/clique' import type { CliqueConsensus } from '../src/consensus/clique' import type { CliqueConfig } from '@ethereumjs/common' +const COMMON = new Common({ chain: Chain.Rinkeby, hardfork: Hardfork.Chainstart }) +const EXTRA_DATA = new Uint8Array(97) +const GAS_LIMIT = BigInt(8000000) + +type Signer = { + address: Address + privateKey: Uint8Array + publicKey: Uint8Array +} + +const A: Signer = { + address: new Address(hexToBytes('0b90087d864e82a284dca15923f3776de6bb016f')), + privateKey: hexToBytes('64bf9cc30328b0e42387b3c82c614e6386259136235e20c1357bd11cdee86993'), + publicKey: hexToBytes( + '40b2ebdf4b53206d2d3d3d59e7e2f13b1ea68305aec71d5d24cefe7f24ecae886d241f9267f04702d7f693655eb7b4aa23f30dcd0c3c5f2b970aad7c8a828195' + ), +} + +const B: Signer = { + address: new Address(hexToBytes('6f62d8382bf2587361db73ceca28be91b2acb6df')), + privateKey: hexToBytes('2a6e9ad5a6a8e4f17149b8bc7128bf090566a11dbd63c30e5a0ee9f161309cd6'), + publicKey: hexToBytes( + 'ca0a55f6e81cb897aee6a1c390aa83435c41048faa0564b226cfc9f3df48b73e846377fb0fd606df073addc7bd851f22547afbbdd5c3b028c91399df802083a2' + ), +} + +const C: Signer = { + address: new Address(hexToBytes('83c30730d1972baa09765a1ac72a43db27fedce5')), + privateKey: hexToBytes('f216ddcf276079043c52b5dd144aa073e6b272ad4bfeaf4fbbc044aa478d1927'), + publicKey: hexToBytes( + '555b19a5cbe6dd082a4a1e1e0520dd52a82ba24fd5598ea31f0f31666c40905ed319314c5fb06d887b760229e1c0e616294e7b1cb5dfefb71507c9112132ce56' + ), +} + +const D: Signer = { + address: new Address(hexToBytes('8458f408106c4875c96679f3f556a511beabe138')), + privateKey: hexToBytes('159e95d07a6c64ddbafa6036cdb7b8114e6e8cdc449ca4b0468a6d0c955f991b'), + publicKey: hexToBytes( + 'f02724341e2df54cf53515f079b1354fa8d437e79c5b091b8d8cc7cbcca00fd8ad854cb3b3a85b06c44ecb7269404a67be88b561f2224c94d133e5fc21be915c' + ), +} + +const E: Signer = { + address: new Address(hexToBytes('ab80a948c661aa32d09952d2a6c4ad77a4c947be')), + privateKey: hexToBytes('48ec5a6c4a7fc67b10a9d4c8a8f594a81ae42e41ed061fa5218d96abb6012344'), + publicKey: hexToBytes( + 'adefb82b9f54e80aa3532263e4478739de16fcca6828f4ae842f8a07941c347fa59d2da1300569237009f0f122dc1fd6abb0db8fcb534280aa94948a5cc95f94' + ), +} + +const F: Signer = { + address: new Address(hexToBytes('dc7bc81ddf67d037d7439f8e6ff12f3d2a100f71')), + privateKey: hexToBytes('86b0ff7b6cf70786f29f297c57562905ab0b6c32d69e177a46491e56da9e486e'), + publicKey: hexToBytes( + 'd3e3d2b722e325bfc085ff5638a112b4e7e88ff13f92fc7f6cfc14b5a25e8d1545a2f27d8537b96e8919949d5f8c139ae7fc81aea7cf7fe5d43d7faaa038e35b' + ), +} + +const initWithSigners = async (signers: Signer[], common?: Common) => { + common = common ?? COMMON + const blocks: Block[] = [] + + const extraData = concatBytes( + new Uint8Array(32), + ...signers.map((s) => s.address.toBytes()), + new Uint8Array(65) + ) + const genesisBlock = Block.fromBlockData( + { header: { gasLimit: GAS_LIMIT, extraData } }, + { common } + ) + blocks.push(genesisBlock) + + const blockchain = await Blockchain.create({ + validateBlocks: true, + validateConsensus: true, + genesisBlock, + common, + }) + return { blocks, blockchain } +} + +function getBlock( + blockchain: Blockchain, + lastBlock: Block, + signer: Signer, + beneficiary?: [Signer, boolean], + checkpointSigners?: Signer[], + common?: Common +) { + common = common ?? COMMON + const number = lastBlock.header.number + BigInt(1) + + let coinbase = Address.zero() + let nonce = CLIQUE_NONCE_DROP + let extraData = EXTRA_DATA + if (beneficiary) { + coinbase = beneficiary[0].address + if (beneficiary[1]) { + nonce = CLIQUE_NONCE_AUTH + } + } else if (checkpointSigners) { + extraData = concatBytes( + new Uint8Array(32), + ...checkpointSigners.map((s) => s.address.toBytes()), + new Uint8Array(65) + ) + } + + const blockData = { + header: { + number, + parentHash: lastBlock.hash(), + coinbase, + timestamp: lastBlock.header.timestamp + BigInt(15), + extraData, + gasLimit: GAS_LIMIT, + difficulty: BigInt(2), + nonce, + }, + } + + // calculate difficulty + const signers = (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(number) + const signerIndex = signers.findIndex((address: Address) => address.equals(signer.address)) + const inTurn = Number(number) % signers.length === signerIndex + blockData.header.difficulty = inTurn ? BigInt(2) : BigInt(1) + + // set signer + const cliqueSigner = signer.privateKey + + return Block.fromBlockData(blockData, { common, freeze: false, cliqueSigner }) +} + +const addNextBlockReorg = async ( + blockchain: Blockchain, + blocks: Block[], + forkBlock: Block, + signer: Signer, + beneficiary?: [Signer, boolean], + checkpointSigners?: Signer[], + common?: Common +) => { + const block = getBlock(blockchain, forkBlock, signer, beneficiary, checkpointSigners, common) + await blockchain.putBlock(block) + blocks.push(block) + return block +} + +const addNextBlock = async ( + blockchain: Blockchain, + blocks: Block[], + signer: Signer, + beneficiary?: [Signer, boolean], + checkpointSigners?: Signer[], + common?: Common +) => { + const block = getBlock( + blockchain, + blocks[blocks.length - 1], + signer, + beneficiary, + checkpointSigners, + common + ) + await blockchain.putBlock(block) + blocks.push(block) + return block +} + tape('Clique: Initialization', (t) => { t.test('should initialize a clique blockchain', async (st) => { const common = new Common({ chain: Chain.Rinkeby, hardfork: Hardfork.Chainstart }) const blockchain = await Blockchain.create({ common }) const head = await blockchain.getIteratorHead() - st.ok(equalsBytes(head.hash(), blockchain.genesisBlock.hash()), 'correct genesis hash') + st.deepEquals(head.hash(), blockchain.genesisBlock.hash(), 'correct genesis hash') st.deepEquals( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(head.header.number + BigInt(1)), head.header.cliqueEpochTransitionSigners(), 'correct genesis signers' ) st.end() }) - const COMMON = new Common({ chain: Chain.Rinkeby, hardfork: Hardfork.Chainstart }) - const EXTRA_DATA = new Uint8Array(97) - const GAS_LIMIT = BigInt(8000000) - - type Signer = { - address: Address - privateKey: Uint8Array - publicKey: Uint8Array - } - - const A: Signer = { - address: new Address(hexToBytes('0b90087d864e82a284dca15923f3776de6bb016f')), - privateKey: hexToBytes('64bf9cc30328b0e42387b3c82c614e6386259136235e20c1357bd11cdee86993'), - publicKey: hexToBytes( - '40b2ebdf4b53206d2d3d3d59e7e2f13b1ea68305aec71d5d24cefe7f24ecae886d241f9267f04702d7f693655eb7b4aa23f30dcd0c3c5f2b970aad7c8a828195' - ), - } - - const B: Signer = { - address: new Address(hexToBytes('6f62d8382bf2587361db73ceca28be91b2acb6df')), - privateKey: hexToBytes('2a6e9ad5a6a8e4f17149b8bc7128bf090566a11dbd63c30e5a0ee9f161309cd6'), - publicKey: hexToBytes( - 'ca0a55f6e81cb897aee6a1c390aa83435c41048faa0564b226cfc9f3df48b73e846377fb0fd606df073addc7bd851f22547afbbdd5c3b028c91399df802083a2' - ), - } - - const C: Signer = { - address: new Address(hexToBytes('83c30730d1972baa09765a1ac72a43db27fedce5')), - privateKey: hexToBytes('f216ddcf276079043c52b5dd144aa073e6b272ad4bfeaf4fbbc044aa478d1927'), - publicKey: hexToBytes( - '555b19a5cbe6dd082a4a1e1e0520dd52a82ba24fd5598ea31f0f31666c40905ed319314c5fb06d887b760229e1c0e616294e7b1cb5dfefb71507c9112132ce56' - ), - } - - const D: Signer = { - address: new Address(hexToBytes('8458f408106c4875c96679f3f556a511beabe138')), - privateKey: hexToBytes('159e95d07a6c64ddbafa6036cdb7b8114e6e8cdc449ca4b0468a6d0c955f991b'), - publicKey: hexToBytes( - 'f02724341e2df54cf53515f079b1354fa8d437e79c5b091b8d8cc7cbcca00fd8ad854cb3b3a85b06c44ecb7269404a67be88b561f2224c94d133e5fc21be915c' - ), - } - - const E: Signer = { - address: new Address(hexToBytes('ab80a948c661aa32d09952d2a6c4ad77a4c947be')), - privateKey: hexToBytes('48ec5a6c4a7fc67b10a9d4c8a8f594a81ae42e41ed061fa5218d96abb6012344'), - publicKey: hexToBytes( - 'adefb82b9f54e80aa3532263e4478739de16fcca6828f4ae842f8a07941c347fa59d2da1300569237009f0f122dc1fd6abb0db8fcb534280aa94948a5cc95f94' - ), - } - - const F: Signer = { - address: new Address(hexToBytes('dc7bc81ddf67d037d7439f8e6ff12f3d2a100f71')), - privateKey: hexToBytes('86b0ff7b6cf70786f29f297c57562905ab0b6c32d69e177a46491e56da9e486e'), - publicKey: hexToBytes( - 'd3e3d2b722e325bfc085ff5638a112b4e7e88ff13f92fc7f6cfc14b5a25e8d1545a2f27d8537b96e8919949d5f8c139ae7fc81aea7cf7fe5d43d7faaa038e35b' - ), - } - - const initWithSigners = async (signers: Signer[], common?: Common) => { - common = common ?? COMMON - const blocks: Block[] = [] - - const extraData = concatBytes( - new Uint8Array(32), - ...signers.map((s) => s.address.toBytes()), - new Uint8Array(65) - ) - const genesisBlock = Block.fromBlockData( - { header: { gasLimit: GAS_LIMIT, extraData } }, - { common } - ) - blocks.push(genesisBlock) - - const blockchain = await Blockchain.create({ - validateBlocks: true, - validateConsensus: true, - genesisBlock, - common, - }) - return { blocks, blockchain } - } - - const addNextBlock = async ( - blockchain: Blockchain, - blocks: Block[], - signer: Signer, - beneficiary?: [Signer, boolean], - checkpointSigners?: Signer[], - common?: Common - ) => { - common = common ?? COMMON - const number = blocks.length - const lastBlock = blocks[number - 1] - - let coinbase = Address.zero() - let nonce = CLIQUE_NONCE_DROP - let extraData = EXTRA_DATA - if (beneficiary) { - coinbase = beneficiary[0].address - if (beneficiary[1]) { - nonce = CLIQUE_NONCE_AUTH - } - } else if (checkpointSigners) { - extraData = concatBytes( - new Uint8Array(32), - ...checkpointSigners.map((s) => s.address.toBytes()), - new Uint8Array(65) - ) - } - - const blockData = { - header: { - number, - parentHash: lastBlock.hash(), - coinbase, - timestamp: lastBlock.header.timestamp + BigInt(15), - extraData, - gasLimit: GAS_LIMIT, - difficulty: BigInt(2), - nonce, - }, - } - - // calculate difficulty - const signers = (blockchain.consensus as CliqueConsensus).cliqueActiveSigners() - const signerIndex = signers.findIndex((address: Address) => address.equals(signer.address)) - const inTurn = number % signers.length === signerIndex - blockData.header.difficulty = inTurn ? BigInt(2) : BigInt(1) - - // set signer - const cliqueSigner = signer.privateKey - - const block = Block.fromBlockData(blockData, { common, freeze: false, cliqueSigner }) - - await blockchain.putBlock(block) - blocks.push(block) - return block - } - t.test('should throw if signer in epoch checkpoint is not active', async (st) => { const { blockchain } = await initWithSigners([A]) ;(blockchain as any)._validateBlocks = false @@ -282,7 +313,12 @@ tape('Clique: Initialization', (t) => { const { blocks, blockchain } = await initWithSigners([A]) const block = await addNextBlock(blockchain, blocks, A) st.equal(block.header.number, BigInt(1)) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [A.address]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + block.header.number + BigInt(1) + ), + [A.address] + ) st.end() }) @@ -292,7 +328,9 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [C, true]) st.deepEqual( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), [A.address, B.address], 'only accept first, second needs 2 votes' ) @@ -310,19 +348,40 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B, [E, true]) st.deepEqual( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), [A.address, B.address, C.address, D.address], 'only accept first two, third needs 3 votes already' ) st.end() }) + t.test('Ensure old clique states are remembered', async (st) => { + const { blocks, blockchain } = await initWithSigners([A, B]) + await addNextBlock(blockchain, blocks, A, [C, true]) + await addNextBlock(blockchain, blocks, B, [C, true]) + await addNextBlock(blockchain, blocks, A, [D, true]) + await addNextBlock(blockchain, blocks, B, [D, true]) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A, [E, true]) + await addNextBlock(blockchain, blocks, B, [E, true]) + await addNextBlock(blockchain, blocks, C) + + for (let i = 1; i < blocks.length; i++) { + await blockchain.putBlock(blocks[i]) + } + st.end() + }) + t.test('Clique Voting: Single signer, dropping itself)', async (st) => { const { blocks, blockchain } = await initWithSigners([A]) await addNextBlock(blockchain, blocks, A, [A, false]) st.deepEqual( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), [], 'weird, but one less cornercase by explicitly allowing this' ) @@ -336,7 +395,9 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, [B, false]) st.deepEqual( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), [A.address, B.address], 'not fulfilled' ) @@ -352,7 +413,9 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B, [B, false]) st.deepEqual( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), [A.address], 'fulfilled' ) @@ -365,10 +428,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address] + ) st.end() }) @@ -379,12 +444,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - C.address, - D.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address, C.address, D.address] + ) st.end() } ) @@ -397,11 +462,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B, [D, false]) await addNextBlock(blockchain, blocks, C, [D, false]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - C.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address, C.address] + ) st.end() } ) @@ -414,10 +480,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [C, true]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address] + ) st.end() }) @@ -432,12 +500,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A) await addNextBlock(blockchain, blocks, B, [C, true]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - C.address, - D.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address, C.address, D.address] + ) st.end() }) @@ -449,10 +517,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [B, false]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address] + ) st.end() }) @@ -470,10 +540,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A) await addNextBlock(blockchain, blocks, B, [C, false]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address] + ) st.end() }) @@ -485,7 +557,9 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, [B, false]) st.deepEqual( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), [A.address, B.address], 'deauth votes' ) @@ -500,7 +574,9 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, [D, true]) st.deepEqual( - (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), [A.address, B.address], 'auth votes' ) @@ -523,10 +599,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A) await addNextBlock(blockchain, blocks, C, [C, true]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address] + ) st.end() } ) @@ -547,11 +625,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A) await addNextBlock(blockchain, blocks, B, [C, true]) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - C.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address, C.address] + ) st.end() } ) @@ -578,13 +657,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, D, [A, false]) await addNextBlock(blockchain, blocks, B, [F, true]) // Finish authorizing F, 3/3 votes needed - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - B.address, - C.address, - D.address, - E.address, - F.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [B.address, C.address, D.address, E.address, F.address] + ) st.end() } ) @@ -614,10 +692,12 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, undefined, [A, B], common) await addNextBlock(blockchain, blocks, B, [C, true], undefined, common) - st.deepEqual((blockchain.consensus as CliqueConsensus).cliqueActiveSigners(), [ - A.address, - B.address, - ]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address] + ) st.end() } ) @@ -698,24 +778,186 @@ tape('Clique: Initialization', (t) => { const { blocks, blockchain } = await initWithSigners([A, B, C]) // block 1: B, next signer: C await addNextBlock(blockchain, blocks, B) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(A.address)) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(B.address)) - st.ok(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(C.address)) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + A.address, + blocks[blocks.length - 1].header.number + ) + ) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + B.address, + blocks[blocks.length - 1].header.number + ) + ) + st.ok( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + C.address, + blocks[blocks.length - 1].header.number + ) + ) // block 2: C, next signer: A await addNextBlock(blockchain, blocks, C) - st.ok(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(A.address)) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(B.address)) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(C.address)) + st.ok( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + A.address, + blocks[blocks.length - 1].header.number + ) + ) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + B.address, + blocks[blocks.length - 1].header.number + ) + ) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + C.address, + blocks[blocks.length - 1].header.number + ) + ) // block 3: A, next signer: B await addNextBlock(blockchain, blocks, A) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(A.address)) - st.ok(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(B.address)) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(C.address)) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + A.address, + blocks[blocks.length - 1].header.number + ) + ) + st.ok( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + B.address, + blocks[blocks.length - 1].header.number + ) + ) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + C.address, + blocks[blocks.length - 1].header.number + ) + ) // block 4: B, next signer: C await addNextBlock(blockchain, blocks, B) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(A.address)) - st.notOk(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(B.address)) - st.ok(await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn(C.address)) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + A.address, + blocks[blocks.length - 1].header.number + ) + ) + st.notOk( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + B.address, + blocks[blocks.length - 1].header.number + ) + ) + st.ok( + await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( + C.address, + blocks[blocks.length - 1].header.number + ) + ) st.end() }) }) + +tape('clique: reorgs', (t) => { + t.test( + 'Two signers, voting to add one other signer, then reorg and revoke this addition', + async (st) => { + const { blocks, blockchain } = await initWithSigners([A, B]) + const genesis = blocks[0] + await addNextBlock(blockchain, blocks, A, [C, true]) + const headBlockUnforked = await addNextBlock(blockchain, blocks, B, [C, true]) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address, C.address], + 'address C added to signers' + ) + st.deepEquals((await blockchain.getCanonicalHeadBlock()).hash(), headBlockUnforked.hash()) + await addNextBlockReorg(blockchain, blocks, genesis, B) + const headBlock = await addNextBlock(blockchain, blocks, A) + st.deepEquals((await blockchain.getCanonicalHeadBlock()).hash(), headBlock.hash()) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A) + + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address], + 'address C not added to signers' + ) + + st.end() + } + ) + + /** + * This test fails, but demonstrates why at an epoch reorg with changing votes, we get an internal error. + t.test( + 'Two signers, voting to add one other signer, epoch transition, then reorg and revoke this addition', + async (st) => { + const common = Common.custom( + { + consensus: { + type: ConsensusType.ProofOfAuthority, + algorithm: ConsensusAlgorithm.Clique, + clique: { + period: 15, + epoch: 3, + }, + }, + }, + { + baseChain: Chain.Rinkeby, + hardfork: Hardfork.Chainstart, + } + ) + const { blocks, blockchain } = await initWithSigners([A, B]) + const genesis = blocks[0] + await addNextBlock(blockchain, blocks, A, [C, true], undefined, common) + await addNextBlock(blockchain, blocks, B, [C, true], undefined, common) + await addNextBlock(blockchain, blocks, A, undefined, undefined, common) + const headBlockUnforked = await addNextBlock( + blockchain, + blocks, + B, + undefined, + undefined, + common + ) + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address, C.address], + 'address C added to signers' + ) + st.deepEquals((await blockchain.getCanonicalHeadBlock()).hash(), headBlockUnforked.hash()) + await addNextBlockReorg(blockchain, blocks, genesis, B, undefined, undefined, common) + await addNextBlock(blockchain, blocks, A, undefined, undefined, common) + + // Add block 3: epoch transition + await addNextBlock(blockchain, blocks, B, undefined, undefined, common) + // Now here suddenly C is added again as signer + + await addNextBlock(blockchain, blocks, A, undefined, undefined, common) + await addNextBlock(blockchain, blocks, B, undefined, undefined, common) + + const headBlock = await addNextBlock(blockchain, blocks, A, undefined, undefined, common) + st.deepEquals((await blockchain.getCanonicalHeadBlock()).hash(), headBlock.hash()) + + st.deepEqual( + (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + blocks[blocks.length - 1].header.number + BigInt(1) + ), + [A.address, B.address], + 'address C not added to signers' + ) + + st.end() + } + ) */ +}) diff --git a/packages/client/lib/miner/miner.ts b/packages/client/lib/miner/miner.ts index ad4ee93637..edb66401d9 100644 --- a/packages/client/lib/miner/miner.ts +++ b/packages/client/lib/miner/miner.ts @@ -106,11 +106,17 @@ export class Miner { // delay signing by rand(SIGNER_COUNT * 500ms) const [signerAddress] = this.config.accounts[0] const { blockchain } = this.service.chain + const parentBlock = this.service.chain.blocks.latest! + //eslint-disable-next-line + const number = parentBlock.header.number + BigInt(1) const inTurn = await (blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( - signerAddress + signerAddress, + number ) if (inTurn === false) { - const signerCount = (blockchain.consensus as CliqueConsensus).cliqueActiveSigners().length + const signerCount = (blockchain.consensus as CliqueConsensus).cliqueActiveSigners( + number + ).length timeout += Math.random() * signerCount * 500 } } @@ -241,7 +247,8 @@ export class Miner { cliqueSigner = signerPrivKey // Determine if signer is INTURN (2) or NOTURN (1) inTurn = await (vmCopy.blockchain.consensus as CliqueConsensus).cliqueSignerInTurn( - signerAddress + signerAddress, + number ) difficulty = inTurn ? 2 : 1 }