From 05b0efac9859ef751a5695da20e67e0a62edbad8 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 6 Jan 2021 11:41:42 +0100 Subject: [PATCH 01/50] common -> clique: added consensus config to chain json files, new Common.consensusConfig() method, added clique parameters for Goerli and Rinkeby, added tests --- packages/common/src/chains/goerli.json | 6 +++++- packages/common/src/chains/kovan.json | 3 ++- packages/common/src/chains/mainnet.json | 3 ++- packages/common/src/chains/rinkeby.json | 6 +++++- packages/common/src/chains/ropsten.json | 3 ++- packages/common/src/index.ts | 15 +++++++++++++++ packages/common/tests/chains.ts | 14 ++++++++++++-- 7 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/common/src/chains/goerli.json b/packages/common/src/chains/goerli.json index f6a3b29607..c0d143159b 100644 --- a/packages/common/src/chains/goerli.json +++ b/packages/common/src/chains/goerli.json @@ -5,7 +5,11 @@ "defaultHardfork": "istanbul", "consensus": { "type": "poa", - "algorithm": "clique" + "algorithm": "clique", + "clique": { + "period": 15, + "epoch": 30000 + } }, "comment": "Cross-client PoA test network", "url": "https://github.com/goerli/testnet", diff --git a/packages/common/src/chains/kovan.json b/packages/common/src/chains/kovan.json index f9b014b859..02d4223042 100644 --- a/packages/common/src/chains/kovan.json +++ b/packages/common/src/chains/kovan.json @@ -5,7 +5,8 @@ "defaultHardfork": "istanbul", "consensus": { "type": "poa", - "algorithm": "aura" + "algorithm": "aura", + "aura": {} }, "comment": "Parity PoA test network", "url": "https://kovan-testnet.github.io/website/", diff --git a/packages/common/src/chains/mainnet.json b/packages/common/src/chains/mainnet.json index 49d34eece8..3e8be34c99 100644 --- a/packages/common/src/chains/mainnet.json +++ b/packages/common/src/chains/mainnet.json @@ -5,7 +5,8 @@ "defaultHardfork": "istanbul", "consensus": { "type": "pow", - "algorithm": "ethash" + "algorithm": "ethash", + "ethash": {} }, "comment": "The Ethereum main chain", "url": "https://ethstats.net/", diff --git a/packages/common/src/chains/rinkeby.json b/packages/common/src/chains/rinkeby.json index 7bc83a3e5e..5aa4b51a10 100644 --- a/packages/common/src/chains/rinkeby.json +++ b/packages/common/src/chains/rinkeby.json @@ -5,7 +5,11 @@ "defaultHardfork": "istanbul", "consensus": { "type": "poa", - "algorithm": "clique" + "algorithm": "clique", + "clique": { + "period": 15, + "epoch": 30000 + } }, "comment": "PoA test network", "url": "https://www.rinkeby.io", diff --git a/packages/common/src/chains/ropsten.json b/packages/common/src/chains/ropsten.json index 84bd62d77a..be6c07449b 100644 --- a/packages/common/src/chains/ropsten.json +++ b/packages/common/src/chains/ropsten.json @@ -5,7 +5,8 @@ "defaultHardfork": "istanbul", "consensus": { "type": "pow", - "algorithm": "ethash" + "algorithm": "ethash", + "ethash": {} }, "comment": "PoW test network", "url": "https://github.com/ethereum/ropsten", diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index d418e5e991..afe4dbcbf7 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -672,4 +672,19 @@ export default class Common { consensusAlgorithm(): string { return (this._chainParams)['consensus']['algorithm'] } + + /** + * Returns a dictionary with consensus configuration + * parameters based on the consensus algorithm + * + * Expected returns (parameters must be present in + * the respective chain json files): + * + * ethash: - + * clique: period, epoch + * aura: - + */ + consensusConfig(): any { + return (this._chainParams)['consensus'][this.consensusAlgorithm()] + } } diff --git a/packages/common/tests/chains.ts b/packages/common/tests/chains.ts index ed162e74c6..4cdae1b724 100644 --- a/packages/common/tests/chains.ts +++ b/packages/common/tests/chains.ts @@ -68,13 +68,23 @@ tape('[Common]: Initialization / Chain params', function (t: tape.Test) { }) t.test('Should provide correct access to chain parameters', function (st: tape.Test) { - const c = new Common({ chain: 'mainnet' }) - const hash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3' + let c = new Common({ chain: 'mainnet' }) + let hash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3' st.equal(c.genesis().hash, hash, 'should return correct genesis hash') st.equal(c.hardforks()[3]['block'], 2463000, 'should return correct hardfork data') st.equal(typeof c.bootstrapNodes()[0].port, 'number', 'should return a port as number') st.equal(c.consensusType(), 'pow', 'should return correct consensus type') st.equal(c.consensusAlgorithm(), 'ethash', 'should return correct consensus algorithm') + st.deepEqual(c.consensusConfig(), {}, 'should return empty dictionary for consensus config') + + c = new Common({ chain: 'rinkeby' }) + hash = '0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177' + st.equal(c.genesis().hash, hash, 'should return correct genesis hash') + st.equal(c.hardforks()[3]['block'], 2, 'should return correct hardfork data') + st.equal(typeof c.bootstrapNodes()[0].port, 'number', 'should return a port as number') + st.equal(c.consensusType(), 'poa', 'should return correct consensus type') + st.equal(c.consensusAlgorithm(), 'clique', 'should return correct consensus algorithm') + st.equal(c.consensusConfig().epoch, 30000, 'should return correct consensus config parameters') st.end() }) From 957256428e65de6316a934b287e7647b5c72a34a Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 6 Jan 2021 11:45:17 +0100 Subject: [PATCH 02/50] common: renamed test files to conform with the *.spec.ts filename convention --- packages/common/package.json | 2 +- packages/common/tests/{chains.ts => chains.spec.ts} | 0 packages/common/tests/{eips.ts => eips.spec.ts} | 0 .../common/tests/{genesisStates.ts => genesisStates.spec.ts} | 0 packages/common/tests/{hardforks.ts => hardforks.spec.ts} | 0 packages/common/tests/{params.ts => params.spec.ts} | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename packages/common/tests/{chains.ts => chains.spec.ts} (100%) rename packages/common/tests/{eips.ts => eips.spec.ts} (100%) rename packages/common/tests/{genesisStates.ts => genesisStates.spec.ts} (100%) rename packages/common/tests/{hardforks.ts => hardforks.spec.ts} (100%) rename packages/common/tests/{params.ts => params.spec.ts} (100%) diff --git a/packages/common/package.json b/packages/common/package.json index 03384b9504..1ca70779c2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -18,7 +18,7 @@ "tsc": "ethereumjs-config-ts-compile", "lint": "ethereumjs-config-lint", "lint:fix": "ethereumjs-config-lint-fix", - "test": "tape -r ts-node/register ./tests/*.ts", + "test": "tape -r ts-node/register ./tests/*.spec.ts", "docs:build": "typedoc --options typedoc.js" }, "repository": { diff --git a/packages/common/tests/chains.ts b/packages/common/tests/chains.spec.ts similarity index 100% rename from packages/common/tests/chains.ts rename to packages/common/tests/chains.spec.ts diff --git a/packages/common/tests/eips.ts b/packages/common/tests/eips.spec.ts similarity index 100% rename from packages/common/tests/eips.ts rename to packages/common/tests/eips.spec.ts diff --git a/packages/common/tests/genesisStates.ts b/packages/common/tests/genesisStates.spec.ts similarity index 100% rename from packages/common/tests/genesisStates.ts rename to packages/common/tests/genesisStates.spec.ts diff --git a/packages/common/tests/hardforks.ts b/packages/common/tests/hardforks.spec.ts similarity index 100% rename from packages/common/tests/hardforks.ts rename to packages/common/tests/hardforks.spec.ts diff --git a/packages/common/tests/params.ts b/packages/common/tests/params.spec.ts similarity index 100% rename from packages/common/tests/params.ts rename to packages/common/tests/params.spec.ts From d6d7c3006e38c76a9bfd61c1f18b8a1eba5cb275 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 6 Jan 2021 12:18:06 +0100 Subject: [PATCH 03/50] block -> clique: added isEpochTransition() method to Block and BlockHeader classes --- packages/block/src/block.ts | 8 ++++++++ packages/block/src/header.ts | 14 ++++++++++++++ packages/block/test/block.spec.ts | 7 +++++++ packages/block/test/header.spec.ts | 19 ++++++++++++++++++- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 3f0c6180f6..bb85de93a8 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -137,6 +137,14 @@ export class Block { return this.header.isGenesis() } + /** + * Checks if the block is an epoch transition + * block (only clique PoA, throws otherwise) + */ + isEpochTransition(): boolean { + return this.header.isEpochTransition() + } + /** * Returns the rlp encoding of the block. */ diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index e249b10f76..cb31f0d721 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -477,6 +477,20 @@ export class BlockHeader { return this.number.isZero() } + /** + * Checks if the block header is an epoch transition + * header (only clique PoA, throws otherwise) + */ + isEpochTransition(): boolean { + if (this._common.consensusAlgorithm() !== 'clique') { + throw new Error('isEpochTransition() call only supported for clique PoA networks') + } + 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 the rlp encoding of the block header. */ diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index 4fdfe3da1f..41d1609a64 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -393,6 +393,13 @@ tape('[Block]: block functions', function (t) { st.end() }) + t.test('should correctly call into header.isEpochTransition()', function (st) { + const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) + const block = Block.fromBlockData({ header: { number: 60000 } }, { common }) + st.ok(block.header.isEpochTransition(), 'should get the header function results') + st.end() + }) + const testDataGenesis = require('./testdata/genesishashestest.json').test t.test('should test genesis hashes (mainnet default)', function (st) { const genesis = Block.genesis() diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index b66b8f4cdf..c331e05e86 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -92,7 +92,7 @@ tape('[Block]: Header functions', function (t) { st.end() }) - t.test('should test isGenesis', function (st) { + t.test('should test isGenesis()', function (st) { const header1 = BlockHeader.fromHeaderData({ number: 1 }) st.equal(header1.isGenesis(), false) @@ -101,6 +101,23 @@ tape('[Block]: Header functions', function (t) { st.end() }) + t.test('should test isEpochTransition()', function (st) { + let header = BlockHeader.fromHeaderData({ number: 1 }) + st.throws(() => { + header.isEpochTransition() + }, 'should throw on PoW networks') + + const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) + header = BlockHeader.genesis({}, { common }) + st.ok(header.isEpochTransition(), 'should indicate an epoch transition for the genesis block') + header = BlockHeader.fromHeaderData({ number: 1 }, { common }) + st.notOk(header.isEpochTransition(), 'should correctly identify a non-epoch block') + header = BlockHeader.fromHeaderData({ number: 60000 }, { common }) + st.ok(header.isEpochTransition(), 'should correctly identify an epoch block') + + st.end() + }) + t.test('should test genesis hashes (mainnet default)', function (st) { const testDataGenesis = require('./testdata/genesishashestest.json').test const header = BlockHeader.genesis() From 17120490e2dd934312c21c9c54207e7fce1d632a Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 6 Jan 2021 12:42:19 +0100 Subject: [PATCH 04/50] block: do not allow passing in uncle header on a PoA network --- packages/block/src/block.ts | 3 +++ packages/block/test/block.spec.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index bb85de93a8..d2fea1c01f 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -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) { diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index 41d1609a64..7ab4215d0d 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -67,6 +67,18 @@ tape('[Block]: block functions', function (t) { st.end() }) }) + t.test( + 'should throw when trying to initialize with uncle headers on a PoA network', + function (st) { + const common = new Common({ chain: 'rinkeby' }) + const uncleBlock = Block.fromBlockData({}, { common }) + + st.throws(function () { + Block.fromBlockData({ uncleHeaders: [uncleBlock.header] }, { common }) + }) + st.end() + } + ) const testData = require('./testdata/testdata.json') From 63c23625a25ec7c19092e1dd18886b3c7b6a6ec3 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 6 Jan 2021 14:29:45 +0100 Subject: [PATCH 05/50] block -> clique: added header timestamp validation checks for clique --- packages/block/src/header.ts | 25 +++++++++++++++++-------- packages/block/test/block.spec.ts | 8 +++----- packages/block/test/header.spec.ts | 27 +++++++++++++++++++++++++++ packages/block/test/util.ts | 10 +++++++++- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index cb31f0d721..36008f72d1 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -393,7 +393,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 @@ -407,33 +408,41 @@ export class BlockHeader { throw new Error('invalid amount of extra data') } - 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') } diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index 7ab4215d0d..2c5d3944a0 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -99,12 +99,10 @@ tape('[Block]: block functions', function (t) { const block = Block.fromRLPSerializedBlock(blockRlp, { common }) const blockchain = new Mockchain() await blockchain.putBlock(Block.fromRLPSerializedBlock(testData.genesisRLP)) - try { + st.doesNotThrow(async () => { await block.validate(blockchain) - } catch (error) { - st.ok(error.toString().match(/block validation is currently only supported on PoW chains/)) - } - st.end() + st.end() + }) }) async function testTransactionValidation(st: tape.Test, block: Block) { diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index c331e05e86..49a8bc134e 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -3,6 +3,8 @@ import { Address, BN, zeros, KECCAK256_RLP, KECCAK256_RLP_ARRAY } from 'ethereum import Common from '@ethereumjs/common' import { BlockHeader } from '../src/header' import { Block } from '../src' +import { Mockchain } from './mockchain' +const testData = require('./testdata/testdata.json') tape('[Block]: Header functions', function (t) { t.test('should create with default constructor', function (st) { @@ -77,6 +79,31 @@ tape('[Block]: Header functions', function (t) { st.end() }) + t.test('header validation -> poa timestamp check', async function (st) { + const headerData = testData.blocks[0].blockHeader + const common = new Common({ chain: 'goerli' }) + const blockchain = new Mockchain() + await blockchain.putBlock(Block.fromRLPSerializedBlock(testData.genesisRLP, { common })) + const msg = 'invalid timestamp diff (lower than period)' + + headerData.timestamp = new BN(1422494850) + let header = BlockHeader.fromHeaderData(headerData, { common }) + try { + await header.validate(blockchain) + } catch (error) { + st.equal(error.message, msg, 'should throw on lower than period diffs') + } + headerData.timestamp = new BN(1422494864) + header = BlockHeader.fromHeaderData(headerData, { common }) + try { + await header.validate(blockchain) + st.pass('should not throw on diff equal to period ') + } catch (error) { + st.notOk('thrown but should not throw') + } + st.end() + }) + t.test('should test validateGasLimit', function (st) { const testData = require('./testdata/bcBlockGasLimitTest.json').tests const bcBlockGasLimitTestData = testData.BlockGasLimit2p63m1 diff --git a/packages/block/test/util.ts b/packages/block/test/util.ts index d69299db2e..9f4edce5a3 100644 --- a/packages/block/test/util.ts +++ b/packages/block/test/util.ts @@ -1,3 +1,4 @@ +import Common from '@ethereumjs/common' import { BN, rlp, keccak256 } from 'ethereumjs-util' import { Block, BlockHeader } from '../src' @@ -7,8 +8,14 @@ import { Block, BlockHeader } from '../src' * @param extraData - Extra data graffiti in order to create equal blocks (like block number) but with different hashes * @param uncles - Optional, an array of uncle headers. Automatically calculates the uncleHash. */ -function createBlock(parentBlock: Block, extraData: string, uncles?: BlockHeader[]): Block { +function createBlock( + parentBlock: Block, + extraData: string, + uncles?: BlockHeader[], + common?: Common +): Block { uncles = uncles ?? [] + common = common ?? new Common({ chain: 'mainnet' }) if (extraData.length > 32) { throw new Error('extra data graffiti must be 32 bytes or less') @@ -27,6 +34,7 @@ function createBlock(parentBlock: Block, extraData: string, uncles?: BlockHeader uncleHeaders: uncles, }, { + common, calcDifficultyFromHeader: parentBlock.header, } ) From a0176480edcd0f9bcb44ffe7f9a4638c9041a99c Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 6 Jan 2021 17:19:43 +0100 Subject: [PATCH 06/50] block -> clique: added extraData length checks for block header, added tests --- packages/block/src/header.ts | 38 +++++++++++++++++++++++++++--- packages/block/test/block.spec.ts | 25 ++++++++++++++------ packages/block/test/header.spec.ts | 19 ++++++++------- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 36008f72d1..c0fcf9d656 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -15,6 +15,9 @@ import { HeaderData, JsonHeader, BlockHeaderBuffer, Blockchain, BlockOptions } f const DEFAULT_GAS_LIMIT = new BN(Buffer.from('ffffffffffffff', 'hex')) +const CLIQUE_EXTRA_VANITY = 32 +const CLIQUE_EXTRA_SEAL = 65 + /** * An object that represents the block header. */ @@ -240,7 +243,7 @@ export class BlockHeader { * Validates correct buffer lengths, throws if invalid. */ _validateBufferLengths() { - const { parentHash, stateRoot, transactionsTrie, receiptTrie, mixHash, nonce } = this + const { parentHash, stateRoot, transactionsTrie, receiptTrie, extraData, mixHash, nonce } = this if (parentHash.length !== 32) { throw new Error(`parentHash must be 32 bytes, received ${parentHash.length} bytes`) } @@ -258,6 +261,31 @@ export class BlockHeader { if (mixHash.length !== 32) { throw new Error(`mixHash must be 32 bytes, received ${mixHash.length} bytes`) } + if (this._common.consensusAlgorithm() === 'clique') { + let minLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL + if (!this.isEpochTransition()) { + if (extraData.length !== minLength) { + throw new Error( + `extraData must be ${minLength} bytes on non-epoch transition blocks, received ${extraData.length} bytes` + ) + } + } else { + // Must contain at least one signer + minLength += 20 + if (extraData.length < minLength) { + throw new Error( + `extraData must be at least ${minLength} bytes on epoch transition blocks, received ${extraData.length} bytes` + ) + } + const signerLength = 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)` + ) + } + } + } + if (nonce.length !== 8) { throw new Error(`nonce must be 8 bytes, received ${nonce.length} bytes`) } @@ -404,8 +432,12 @@ 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') + } } const parentHeader = await this._getHeaderByHash(blockchain, this.parentHash) diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index 2c5d3944a0..a63204f6d3 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -71,7 +71,10 @@ tape('[Block]: block functions', function (t) { 'should throw when trying to initialize with uncle headers on a PoA network', function (st) { const common = new Common({ chain: 'rinkeby' }) - const uncleBlock = Block.fromBlockData({}, { common }) + const uncleBlock = Block.fromBlockData( + { header: { extraData: Buffer.alloc(117) } }, + { common } + ) st.throws(function () { Block.fromBlockData({ uncleHeaders: [uncleBlock.header] }, { common }) @@ -86,24 +89,29 @@ tape('[Block]: block functions', function (t) { const blockRlp = testData.blocks[0].rlp const block = Block.fromRLPSerializedBlock(blockRlp) const blockchain = new Mockchain() - await blockchain.putBlock(Block.fromRLPSerializedBlock(testData.genesisRLP)) + const genesisBlock = Block.fromRLPSerializedBlock(testData.genesisRLP) + await blockchain.putBlock(genesisBlock) st.doesNotThrow(async () => { await block.validate(blockchain) st.end() }) }) - t.test('should test block validation on poa chain', async function (st) { + // TODO: reactivate test using dedicated Rinkeby testdata for PoA tests + // (extract from client output once Goerli or Rinkeby connection possible with Block.toJSON()) + /*t.test('should test block validation on poa chain', async function (st) { + const common = new Common({ chain: 'goerli', hardfork: 'chainstart' }) const blockRlp = testData.blocks[0].rlp - const common = new Common({ chain: 'goerli' }) const block = Block.fromRLPSerializedBlock(blockRlp, { common }) const blockchain = new Mockchain() - await blockchain.putBlock(Block.fromRLPSerializedBlock(testData.genesisRLP)) + const genesisBlock = Block.fromRLPSerializedBlock(testData.genesisRLP, { common }) + console.log(genesisBlock.header.hash()) + await blockchain.putBlock(genesisBlock) st.doesNotThrow(async () => { await block.validate(blockchain) st.end() }) - }) + })*/ async function testTransactionValidation(st: tape.Test, block: Block) { st.ok(block.validateTransactions()) @@ -405,7 +413,10 @@ tape('[Block]: block functions', function (t) { t.test('should correctly call into header.isEpochTransition()', function (st) { const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) - const block = Block.fromBlockData({ header: { number: 60000 } }, { common }) + const block = Block.fromBlockData( + { header: { number: 60000, extraData: Buffer.alloc(117) } }, + { common } + ) st.ok(block.header.isEpochTransition(), 'should get the header function results') st.end() }) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 49a8bc134e..563e5f1d47 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -3,8 +3,8 @@ import { Address, BN, zeros, KECCAK256_RLP, KECCAK256_RLP_ARRAY } from 'ethereum import Common from '@ethereumjs/common' import { BlockHeader } from '../src/header' import { Block } from '../src' -import { Mockchain } from './mockchain' -const testData = require('./testdata/testdata.json') +//import { Mockchain } from './mockchain' +//const testData = require('./testdata/testdata.json') tape('[Block]: Header functions', function (t) { t.test('should create with default constructor', function (st) { @@ -79,7 +79,9 @@ tape('[Block]: Header functions', function (t) { st.end() }) - t.test('header validation -> poa timestamp check', async function (st) { + // TODO: reactivate test using dedicated Rinkeby testdata for PoA tests + // (extract from client output once Goerli or Rinkeby connection possible with Block.toJSON()) + /*t.test('header validation -> poa checks', async function (st) { const headerData = testData.blocks[0].blockHeader const common = new Common({ chain: 'goerli' }) const blockchain = new Mockchain() @@ -87,22 +89,23 @@ tape('[Block]: Header functions', function (t) { const msg = 'invalid timestamp diff (lower than period)' headerData.timestamp = new BN(1422494850) + headerData.extraData = Buffer.alloc(97) let header = BlockHeader.fromHeaderData(headerData, { common }) try { await header.validate(blockchain) } catch (error) { - st.equal(error.message, msg, 'should throw on lower than period diffs') + st.equal(error.message, msg, 'should throw on lower than period timestamp diffs') } headerData.timestamp = new BN(1422494864) header = BlockHeader.fromHeaderData(headerData, { common }) try { await header.validate(blockchain) - st.pass('should not throw on diff equal to period ') + st.pass('should not throw on timestamp diff equal to period ') } catch (error) { st.notOk('thrown but should not throw') } st.end() - }) + })*/ t.test('should test validateGasLimit', function (st) { const testData = require('./testdata/bcBlockGasLimitTest.json').tests @@ -137,9 +140,9 @@ tape('[Block]: Header functions', function (t) { const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) header = BlockHeader.genesis({}, { common }) st.ok(header.isEpochTransition(), 'should indicate an epoch transition for the genesis block') - header = BlockHeader.fromHeaderData({ number: 1 }, { common }) + header = BlockHeader.fromHeaderData({ number: 1, extraData: Buffer.alloc(97) }, { common }) st.notOk(header.isEpochTransition(), 'should correctly identify a non-epoch block') - header = BlockHeader.fromHeaderData({ number: 60000 }, { common }) + header = BlockHeader.fromHeaderData({ number: 60000, extraData: Buffer.alloc(117) }, { common }) st.ok(header.isEpochTransition(), 'should correctly identify an epoch block') st.end() From 7cd888d753664406167b9cf71f9ba4223c37f6f8 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Sat, 9 Jan 2021 16:11:37 +0100 Subject: [PATCH 07/50] block -> clique: added helper functions cliqueExtraVanity(), cliqueExtraSeal(), cliqueEpochTransitionSigners() --- packages/block/src/block.ts | 32 +++++++++++++++-- packages/block/src/header.ts | 56 +++++++++++++++++++++++++++--- packages/block/test/block.spec.ts | 22 ++++++++++-- packages/block/test/header.spec.ts | 51 +++++++++++++++++++++++---- 4 files changed, 144 insertions(+), 17 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index d2fea1c01f..a584111367 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -144,8 +144,36 @@ export class Block { * Checks if the block is an epoch transition * block (only clique PoA, throws otherwise) */ - isEpochTransition(): boolean { - return this.header.isEpochTransition() + 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(): Buffer[] { + return this.header.cliqueEpochTransitionSigners() } /** diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index c0fcf9d656..0e2d92c8f3 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -263,7 +263,7 @@ export class BlockHeader { } if (this._common.consensusAlgorithm() === 'clique') { let minLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL - if (!this.isEpochTransition()) { + if (!this.cliqueIsEpochTransition()) { if (extraData.length !== minLength) { throw new Error( `extraData must be ${minLength} bytes on non-epoch transition blocks, received ${extraData.length} bytes` @@ -518,20 +518,66 @@ export class BlockHeader { return this.number.isZero() } + private _checkClique() { + if (this._common.consensusAlgorithm() !== 'clique') { + throw new Error('Function call only supported for clique PoA networks') + } + } + /** * Checks if the block header is an epoch transition * header (only clique PoA, throws otherwise) */ - isEpochTransition(): boolean { - if (this._common.consensusAlgorithm() !== 'clique') { - throw new Error('isEpochTransition() call only supported for clique PoA networks') - } + cliqueIsEpochTransition(): boolean { + this._checkClique() 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._checkClique() + return this.extraData.slice(0, CLIQUE_EXTRA_VANITY) + } + + /** + * Returns extra seal data + * (only clique PoA, throws otherwise) + */ + cliqueExtraSeal(): Buffer { + this._checkClique() + 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(): Buffer[] { + this._checkClique() + if (!this.cliqueIsEpochTransition()) { + throw new Error('Singers are only included in epoch transition blocks (clique)') + } + let signerBuffer = this.extraData.slice(CLIQUE_EXTRA_VANITY) + signerBuffer = signerBuffer.slice(0, signerBuffer.length - CLIQUE_EXTRA_SEAL) + + 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 + } + /** * Returns the rlp encoding of the block header. */ diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index a63204f6d3..b4274dd398 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -411,13 +411,29 @@ tape('[Block]: block functions', function (t) { st.end() }) - t.test('should correctly call into header.isEpochTransition()', function (st) { + t.test('should correctly call into header clique functions', function (st) { const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) const block = Block.fromBlockData( - { header: { number: 60000, extraData: Buffer.alloc(117) } }, + { header: { number: 60000, extraData: Buffer.alloc(137) } }, { common } ) - st.ok(block.header.isEpochTransition(), 'should get the header function results') + st.ok( + block.cliqueIsEpochTransition(), + 'header.cliqueIsEpochTransition() -> should get the header function results' + ) + st.deepEqual( + block.cliqueExtraVanity(), + Buffer.alloc(32), + 'header.cliqueExtraVanity() -> should get the header function results' + ) + st.deepEqual( + block.cliqueExtraSeal(), + Buffer.alloc(65), + 'header.cliqueExtraSeal() -> should get the header function results' + ) + const msg = 'header.cliqueEpochTransitionSigners() -> should get the header function results' + st.deepEqual(block.cliqueEpochTransitionSigners(), [Buffer.alloc(20), Buffer.alloc(20)], msg) + st.end() }) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 563e5f1d47..4c3b43a5fc 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -131,19 +131,56 @@ tape('[Block]: Header functions', function (t) { st.end() }) - t.test('should test isEpochTransition()', function (st) { + t.test('should test clique functionality', function (st) { let header = BlockHeader.fromHeaderData({ number: 1 }) st.throws(() => { - header.isEpochTransition() - }, 'should throw on PoW networks') + header.cliqueIsEpochTransition() + }, 'cliqueIsEpochTransition() -> should throw on PoW networks') const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) header = BlockHeader.genesis({}, { common }) - st.ok(header.isEpochTransition(), 'should indicate an epoch transition for the genesis block') + st.ok( + header.cliqueIsEpochTransition(), + 'cliqueIsEpochTransition() -> should indicate an epoch transition for the genesis block' + ) + header = BlockHeader.fromHeaderData({ number: 1, extraData: Buffer.alloc(97) }, { common }) - st.notOk(header.isEpochTransition(), 'should correctly identify a non-epoch block') - header = BlockHeader.fromHeaderData({ number: 60000, extraData: Buffer.alloc(117) }, { common }) - st.ok(header.isEpochTransition(), 'should correctly identify an epoch block') + st.notOk( + header.cliqueIsEpochTransition(), + 'cliqueIsEpochTransition() -> should correctly identify a non-epoch block' + ) + st.deepEqual( + header.cliqueExtraVanity(), + Buffer.alloc(32), + 'cliqueExtraVanity() -> should return correct extra vanity value' + ) + st.deepEqual( + header.cliqueExtraSeal(), + Buffer.alloc(65), + 'cliqueExtraSeal() -> should return correct extra seal value' + ) + st.throws(() => { + header.cliqueEpochTransitionSigners() + }, 'cliqueEpochTransitionSigners() -> should throw on non-epch block') + + header = BlockHeader.fromHeaderData({ number: 60000, extraData: Buffer.alloc(137) }, { common }) + st.ok( + header.cliqueIsEpochTransition(), + 'cliqueIsEpochTransition() -> should correctly identify an epoch block' + ) + st.deepEqual( + header.cliqueExtraVanity(), + Buffer.alloc(32), + 'cliqueExtraVanity() -> should return correct extra vanity value' + ) + st.deepEqual( + header.cliqueExtraSeal(), + Buffer.alloc(65), + 'cliqueExtraSeal() -> should return correct extra seal value' + ) + const msg = + 'cliqueEpochTransitionSigners() -> should return the correct epoch transition signer list on epoch block' + st.deepEqual(header.cliqueEpochTransitionSigners(), [Buffer.alloc(20), Buffer.alloc(20)], msg) st.end() }) From 0873bb7d3487c85bbcca1141e61f0a9597eb2b0b Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Sat, 9 Jan 2021 17:33:22 +0100 Subject: [PATCH 08/50] block -> clique: added checks for coinbase and mixHash --- packages/block/src/header.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 0e2d92c8f3..6dec7cb28a 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -223,7 +223,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 @@ -242,7 +242,7 @@ export class BlockHeader { /** * Validates correct buffer lengths, throws if invalid. */ - _validateBufferLengths() { + _validateHeaderFields() { const { parentHash, stateRoot, transactionsTrie, receiptTrie, extraData, mixHash, nonce } = this if (parentHash.length !== 32) { throw new Error(`parentHash must be 32 bytes, received ${parentHash.length} bytes`) @@ -261,14 +261,20 @@ export class BlockHeader { if (mixHash.length !== 32) { throw new Error(`mixHash must be 32 bytes, received ${mixHash.length} bytes`) } + // PoA Clique specfific validations if (this._common.consensusAlgorithm() === 'clique') { let minLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL if (!this.cliqueIsEpochTransition()) { + // ExtraData length on epoch transition if (extraData.length !== minLength) { throw new Error( `extraData must be ${minLength} bytes on non-epoch transition blocks, received ${extraData.length} bytes` ) } + // 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()}`) + } } else { // Must contain at least one signer minLength += 20 @@ -284,6 +290,10 @@ export class BlockHeader { ) } } + // MixHash format + if (!this.mixHash.equals(Buffer.alloc(32))) { + throw new Error(`mixHash must be filled with zeros, received ${this.mixHash}`) + } } if (nonce.length !== 8) { From 2df16e07e9740942496240751465e0509aaa73a6 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Sat, 9 Jan 2021 18:40:51 +0100 Subject: [PATCH 09/50] block -> clique: adopted hash() method for clique blocks, added DRAFT cliqueVerifySignature() method --- packages/block/src/header.ts | 43 ++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 6dec7cb28a..b06613d39a 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -3,6 +3,7 @@ import { Address, BN, bnToHex, + ecrecover, KECCAK256_RLP_ARRAY, KECCAK256_RLP, rlp, @@ -10,6 +11,7 @@ import { toBuffer, unpadBuffer, zeros, + bufferToInt, } from 'ethereumjs-util' import { HeaderData, JsonHeader, BlockHeaderBuffer, Blockchain, BlockOptions } from './types' @@ -273,7 +275,9 @@ export class BlockHeader { } // 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()}`) + throw new Error( + `coinbase must be filled with zeros on epoch transition blocks, received ${this.coinbase.toString()}` + ) } } else { // Must contain at least one signer @@ -495,6 +499,14 @@ export class BlockHeader { * Returns a Buffer Array of the raw Buffers in this header, in order. */ raw(): BlockHeaderBuffer { + let extraData + // Hash for PoA clique blocks is created without the seal + if (this._common.consensusAlgorithm() === 'clique') { + extraData = this.extraData.slice(0, this.extraData.length - CLIQUE_EXTRA_SEAL) + } else { + extraData = this.extraData + } + return [ this.parentHash, this.uncleHash, @@ -508,7 +520,7 @@ export class BlockHeader { unpadBuffer(toBuffer(this.gasLimit)), unpadBuffer(toBuffer(this.gasUsed)), unpadBuffer(toBuffer(this.timestamp)), - this.extraData, + extraData, this.mixHash, this.nonce, ] @@ -588,6 +600,33 @@ export class BlockHeader { return signerList } + /** + * Verifies the signature of the block (last 65 bytes of extraData field) + * (only clique PoA, throws otherwise) + * + * Method throws if signature is invalid + * + * DRAFT: METHOD IN DRAFT STATE, NEEDS THOROUGH TESTING + */ + cliqueVerifySignature(signerList: Buffer[]): void { + this._checkClique() + const extraSeal = this.cliqueExtraSeal() + const r = extraSeal.slice(0, 32) + const s = extraSeal.slice(32, 64) + const v = bufferToInt(extraSeal.slice(64, 65)) + const pubKey = ecrecover(this.hash(), v, r, s) + const address = Address.fromPublicKey(pubKey) + if ( + !signerList.find((signer) => { + return signer.equals(address.toBuffer()) + }) + ) { + throw new Error( + `block signature validation failed (clique), ${address.toString()} not in signer list` + ) + } + } + /** * Returns the rlp encoding of the block header. */ From a28851b8ca3d326ac31c14faa93cafbe5a99b692 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Sun, 10 Jan 2021 12:26:38 +0100 Subject: [PATCH 10/50] blockchain -> clique: data structure preparations for signer lists and voting --- packages/blockchain/src/index.ts | 48 ++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index c75117221d..cc552d0869 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -10,6 +10,12 @@ import { DBTarget } from './db/operation' import type { LevelUp } from 'levelup' const level = require('level-mem') +type CliqueLatestSignerStates = [[Buffer, Buffer[]]] +type CliqueLatestVotes = [[Buffer, [Buffer, Buffer, Buffer]]] + +// const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') +// const CLIQUE_NONCE_DROP = Buffer.alloc(8) + type OnBlock = (block: Block, reorg: boolean) => Promise | void export interface BlockchainInterface { @@ -96,8 +102,6 @@ export default class Blockchain implements BlockchainInterface { db: LevelUp dbManager: DBManager - _ethash?: Ethash - private _genesis?: Buffer // the genesis hash of this blockchain // The following two heads and the heads stored within the `_heads` always point @@ -118,6 +122,46 @@ export default class Blockchain implements BlockchainInterface { private readonly _validateConsensus: boolean private readonly _validateBlocks: boolean + _ethash?: Ethash + + /** + * Keep signer history data (signer states and votes) for all + * block numbers >= HEAD_BLOCK - CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT + * + * This defines a limit for reorgs on PoA clique chains + */ + private CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 100 + + /** + * List with the latest signer states checkpointed on blocks where + * a change (added new or removed a signer) occurred. + * + * Format: + * [ [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 + * + * Always keep at least one item on the stack + */ + private _cliqueLatestSignerStates?: CliqueLatestSignerStates + + /** + * List with the latest signer votes + * + * Format: + * [ [BLOCK_NUMBER_1, [SIGNER, BENEFICIARY, AUTH]], [BLOCK_NUMBER_1, [SIGNER, BENEFICIARY, AUTH]] ] + * where AUTH = CLIQUE_NONCE_AUTH | CLIQUE_NONCE_DROP + * + * For votes all elements here must be taken into account with a + * block number >= LAST_EPOCH_BLOCK + * (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. + */ + private _cliqueLatestVotes?: CliqueLatestVotes + /** * Creates new Blockchain object * From 0aa3f29d0ca322541246dbd9d813889f652e5ac2 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Sun, 10 Jan 2021 16:04:24 +0100 Subject: [PATCH 11/50] blockchain -> clique: draft signer integration --- packages/block/src/header.ts | 14 ++++++------- packages/blockchain/src/db/constants.ts | 6 ++++++ packages/blockchain/src/db/manager.ts | 7 +++++++ packages/blockchain/src/db/operation.ts | 6 ++++++ packages/blockchain/src/index.ts | 26 ++++++++++++++++++------- packages/blockchain/test/clique.ts | 12 ++++++++++++ 6 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 packages/blockchain/test/clique.ts diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index b06613d39a..bd80e3a5d3 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -273,13 +273,13 @@ export class BlockHeader { `extraData must be ${minLength} bytes on non-epoch transition blocks, received ${extraData.length} bytes` ) } - // coinbase (beneficiary) on epoch transition - if (!this.coinbase.isZero()) { + } else { + const signerLength = extraData.length - minLength + if (signerLength % 20 !== 0) { throw new Error( - `coinbase must be filled with zeros on epoch transition blocks, received ${this.coinbase.toString()}` + `invalid signer list length in extraData, received signer length of ${signerLength} (not divisible by 20)` ) } - } else { // Must contain at least one signer minLength += 20 if (extraData.length < minLength) { @@ -287,10 +287,10 @@ export class BlockHeader { `extraData must be at least ${minLength} bytes on epoch transition blocks, received ${extraData.length} bytes` ) } - const signerLength = extraData.length - minLength - if (signerLength % 20 !== 0) { + // coinbase (beneficiary) on epoch transition + if (!this.coinbase.isZero()) { throw new Error( - `invalid signer list length in extraData, received signer length of ${signerLength} (not divisible by 20)` + `coinbase must be filled with zeros on epoch transition blocks, received ${this.coinbase.toString()}` ) } } diff --git a/packages/blockchain/src/db/constants.ts b/packages/blockchain/src/db/constants.ts index 00a8bd4548..59a06e258e 100644 --- a/packages/blockchain/src/db/constants.ts +++ b/packages/blockchain/src/db/constants.ts @@ -14,6 +14,11 @@ const HEAD_HEADER_KEY = 'LastHeader' */ const HEAD_BLOCK_KEY = 'LastBlock' +/** + * Cique signers + */ +const CLIQUE_SIGNERS_KEY = 'CliqueSigners' + /** * headerPrefix + number + hash -> header */ @@ -63,6 +68,7 @@ export { HEADS_KEY, HEAD_HEADER_KEY, HEAD_BLOCK_KEY, + CLIQUE_SIGNERS_KEY, bufBE8, tdKey, headerKey, diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index 987de8b143..f3854bdaf3 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -73,6 +73,13 @@ export class DBManager { return this.get(DBTarget.HeadBlock) } + /** + * Fetches clique signers. + */ + async getCliqueSigners(): Promise { + return this.get(DBTarget.CliqueSigners) + } + /** * Fetches a block (header and body) given a block id, * which can be either its hash or its number. diff --git a/packages/blockchain/src/db/operation.ts b/packages/blockchain/src/db/operation.ts index 5a16117ae3..bd778cfd19 100644 --- a/packages/blockchain/src/db/operation.ts +++ b/packages/blockchain/src/db/operation.ts @@ -8,6 +8,7 @@ import { bodyKey, numberToHashKey, hashToNumberKey, + CLIQUE_SIGNERS_KEY, } from './constants' import { CacheMap } from './manager' @@ -19,6 +20,7 @@ export enum DBTarget { HashToNumber, NumberToHash, TotalDifficulty, + CliqueSigners, Body, Header, } @@ -72,6 +74,10 @@ export class DBOp { this.baseDBOp.key = HEAD_BLOCK_KEY break } + case DBTarget.CliqueSigners: { + this.baseDBOp.key = CLIQUE_SIGNERS_KEY + break + } case DBTarget.HashToNumber: { this.baseDBOp.key = hashToNumberKey(key!.blockHash!) this.cacheString = 'hashToNumber' diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index cc552d0869..7ec84b2c50 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -1,5 +1,5 @@ import Semaphore from 'semaphore-async-await' -import { BN } from 'ethereumjs-util' +import { BN, rlp } from 'ethereumjs-util' import { Block, BlockHeader } from '@ethereumjs/block' import Ethash from '@ethereumjs/ethash' import Common from '@ethereumjs/common' @@ -88,9 +88,9 @@ export interface BlockchainOptions { /** * The blockchain only initializes succesfully if it has a genesis block. If * there is no block available in the DB and a `genesisBlock` is provided, - * then the provided `genesisBlock` will be used as genesis If no block is + * then the provided `genesisBlock` will be used as genesis. If no block is * present in the DB and no block is provided, then the genesis block as - * provided from the `common` will be used + * provided from the `common` will be used. */ genesisBlock?: Block } @@ -251,10 +251,10 @@ export default class Blockchain implements BlockchainInterface { * @hidden */ private async _init(genesisBlock?: Block): Promise { - let DBGenesisBlock + let dbGenesisBlock try { const genesisHash = await this.dbManager.numberToHash(new BN(0)) - DBGenesisBlock = await this.dbManager.getBlock(genesisHash) + dbGenesisBlock = await this.dbManager.getBlock(genesisHash) } catch (error) { if (error.type !== 'NotFoundError') { throw error @@ -271,7 +271,7 @@ export default class Blockchain implements BlockchainInterface { // If the DB has a genesis block, then verify that the genesis block in the // DB is indeed the Genesis block generated or assigned. - if (DBGenesisBlock && !genesisBlock.hash().equals(DBGenesisBlock.hash())) { + if (dbGenesisBlock && !genesisBlock.hash().equals(dbGenesisBlock.hash())) { throw new Error( 'The genesis block in the DB has a different hash than the provided genesis block.' ) @@ -279,16 +279,28 @@ export default class Blockchain implements BlockchainInterface { const genesisHash = genesisBlock.hash() - if (!DBGenesisBlock) { + if (!dbGenesisBlock) { // If there is no genesis block put the genesis block in the DB. // For that TD, the BlockOrHeader, and the Lookups have to be saved. const dbOps: DBOp[] = [] dbOps.push(DBSetTD(genesisBlock.header.difficulty.clone(), new BN(0), genesisHash)) DBSetBlockOrHeader(genesisBlock).map((op) => dbOps.push(op)) DBSaveLookups(genesisHash, new BN(0)).map((op) => dbOps.push(op)) + + // Clique: save initial genesis block signers to DB + if (this._common.consensusAlgorithm() === 'clique') { + const signers = genesisBlock.header.cliqueEpochTransitionSigners() + dbOps.push(DBOp.set(DBTarget.CliqueSigners, rlp.encode(signers))) + } await this.dbManager.batch(dbOps) } + // Clique: read current signer list + if (this._common.consensusAlgorithm() === 'clique') { + const signersB = await this.dbManager.getCliqueSigners() + this._cliqueLatestSignerStates = (rlp.decode(signersB)) + } + // At this point, we can safely set genesisHash as the _genesis hash in this // object: it is either the one we put in the DB, or it is equal to the one // which we read from the DB. diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.ts new file mode 100644 index 0000000000..863abc3a3e --- /dev/null +++ b/packages/blockchain/test/clique.ts @@ -0,0 +1,12 @@ +import Common from '@ethereumjs/common' +import tape from 'tape' +import Blockchain from '../src' + +tape('Clique', (t) => { + t.test('should initialize a clique blockchain', async (st) => { + const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) + const blockchain = new Blockchain({ common, validateConsensus: false }) + await blockchain.getHead() + st.end() + }) +}) From 29c11e6101bfb104f76bc70ef62e4249246f66a3 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Mon, 11 Jan 2021 10:06:05 +0100 Subject: [PATCH 12/50] block -> clique: moved extra data checks from constructor to validate() (being too strict on default blocks), only adopt extraData for hash() and not in the raw() method --- packages/block/src/header.ts | 81 +++++++++++++----------------- packages/block/test/header.spec.ts | 16 +++++- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index bd80e3a5d3..3b917d209e 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -245,7 +245,7 @@ export class BlockHeader { * Validates correct buffer lengths, throws if invalid. */ _validateHeaderFields() { - const { parentHash, stateRoot, transactionsTrie, receiptTrie, extraData, mixHash, nonce } = this + const { parentHash, stateRoot, transactionsTrie, receiptTrie, mixHash, nonce } = this if (parentHash.length !== 32) { throw new Error(`parentHash must be 32 bytes, received ${parentHash.length} bytes`) } @@ -263,42 +263,6 @@ export class BlockHeader { if (mixHash.length !== 32) { throw new Error(`mixHash must be 32 bytes, received ${mixHash.length} bytes`) } - // PoA Clique specfific validations - if (this._common.consensusAlgorithm() === 'clique') { - let minLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL - if (!this.cliqueIsEpochTransition()) { - // ExtraData length on epoch transition - if (extraData.length !== minLength) { - throw new Error( - `extraData must be ${minLength} bytes on non-epoch transition blocks, received ${extraData.length} bytes` - ) - } - } else { - const signerLength = 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)` - ) - } - // Must contain at least one signer - minLength += 20 - if (extraData.length < minLength) { - throw new Error( - `extraData must be at least ${minLength} bytes on epoch transition blocks, received ${extraData.length} bytes` - ) - } - // 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 (nonce.length !== 8) { throw new Error(`nonce must be 8 bytes, received ${nonce.length} bytes`) @@ -452,6 +416,33 @@ export class BlockHeader { ) { 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}`) + } } const parentHeader = await this._getHeaderByHash(blockchain, this.parentHash) @@ -499,14 +490,6 @@ export class BlockHeader { * Returns a Buffer Array of the raw Buffers in this header, in order. */ raw(): BlockHeaderBuffer { - let extraData - // Hash for PoA clique blocks is created without the seal - if (this._common.consensusAlgorithm() === 'clique') { - extraData = this.extraData.slice(0, this.extraData.length - CLIQUE_EXTRA_SEAL) - } else { - extraData = this.extraData - } - return [ this.parentHash, this.uncleHash, @@ -520,7 +503,7 @@ export class BlockHeader { unpadBuffer(toBuffer(this.gasLimit)), unpadBuffer(toBuffer(this.gasUsed)), unpadBuffer(toBuffer(this.timestamp)), - extraData, + this.extraData, this.mixHash, this.nonce, ] @@ -530,6 +513,12 @@ export class BlockHeader { * Returns the hash of the block header. */ hash(): Buffer { + const raw = this.raw() + // Hash for PoA clique blocks is created without the seal + if (this._common.consensusAlgorithm() === 'clique') { + raw[12] = this.extraData.slice(0, this.extraData.length - CLIQUE_EXTRA_SEAL) + } + return rlphash(this.raw()) } diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 4c3b43a5fc..3a1de0635a 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -38,7 +38,10 @@ tape('[Block]: Header functions', function (t) { t.test('should test header initialization', function (st) { const common = new Common({ chain: 'ropsten', hardfork: 'chainstart' }) let header = BlockHeader.genesis(undefined, { common }) - st.ok(header.hash().toString('hex'), 'block should initialize') + st.ok(header.hash().toString('hex'), 'genesis block should initialize') + + header = BlockHeader.fromHeaderData({}, { common }) + st.ok(header.hash().toString('hex'), 'default block should initialize') // test default freeze values // also test if the options are carried over to the constructor @@ -79,6 +82,17 @@ tape('[Block]: Header functions', function (t) { st.end() }) + t.test('should test header initialization -> clique', function (st) { + const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) + let header = BlockHeader.genesis(undefined, { common }) + st.ok(header.hash().toString('hex'), 'genesis block should initialize') + + header = BlockHeader.fromHeaderData({}, { common }) + st.ok(header.hash().toString('hex'), 'default block should initialize') + + st.end() + }) + // TODO: reactivate test using dedicated Rinkeby testdata for PoA tests // (extract from client output once Goerli or Rinkeby connection possible with Block.toJSON()) /*t.test('header validation -> poa checks', async function (st) { From 1557f5c59fd8eebd000145ffaa5de5528c79c652 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Mon, 11 Jan 2021 11:04:48 +0100 Subject: [PATCH 13/50] blockchain -> clique: init blockchain with the state of active signers read from genesis, save signer states to DB --- packages/blockchain/src/db/manager.ts | 6 ++-- packages/blockchain/src/db/operation.ts | 8 ++--- packages/blockchain/src/index.ts | 42 ++++++++++++++++++++----- packages/blockchain/test/clique.ts | 10 +++++- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index f3854bdaf3..3c9186d12c 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -12,6 +12,7 @@ import Cache from './cache' import { DatabaseKey, DBOp, DBTarget, DBOpData } from './operation' import type { LevelUp } from 'levelup' +import { CliqueLatestSignerStates } from '..' const level = require('level-mem') @@ -76,8 +77,9 @@ export class DBManager { /** * Fetches clique signers. */ - async getCliqueSigners(): Promise { - return this.get(DBTarget.CliqueSigners) + async getCliqueLatestSignerStates(): Promise { + const signerStates = await this.get(DBTarget.CliqueSignerStates) + return (rlp.decode(signerStates)) } /** diff --git a/packages/blockchain/src/db/operation.ts b/packages/blockchain/src/db/operation.ts index bd778cfd19..ddbcebfa82 100644 --- a/packages/blockchain/src/db/operation.ts +++ b/packages/blockchain/src/db/operation.ts @@ -8,7 +8,7 @@ import { bodyKey, numberToHashKey, hashToNumberKey, - CLIQUE_SIGNERS_KEY, + CLIQUE_SIGNERS_KEY as CLIQUE_SIGNER_STATES_KEY, } from './constants' import { CacheMap } from './manager' @@ -20,7 +20,7 @@ export enum DBTarget { HashToNumber, NumberToHash, TotalDifficulty, - CliqueSigners, + CliqueSignerStates, Body, Header, } @@ -74,8 +74,8 @@ export class DBOp { this.baseDBOp.key = HEAD_BLOCK_KEY break } - case DBTarget.CliqueSigners: { - this.baseDBOp.key = CLIQUE_SIGNERS_KEY + case DBTarget.CliqueSignerStates: { + this.baseDBOp.key = CLIQUE_SIGNER_STATES_KEY break } case DBTarget.HashToNumber: { diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 7ec84b2c50..e665aadfc4 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -10,8 +10,10 @@ import { DBTarget } from './db/operation' import type { LevelUp } from 'levelup' const level = require('level-mem') -type CliqueLatestSignerStates = [[Buffer, Buffer[]]] -type CliqueLatestVotes = [[Buffer, [Buffer, Buffer, Buffer]]] +export type CliqueSignerState = [Buffer, Buffer[]] +export type CliqueLatestSignerStates = CliqueSignerState[] +export type CliqueVotes = [Buffer, [Buffer, Buffer, Buffer]] +export type CliqueLatestVotes = CliqueVotes[] // const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') // const CLIQUE_NONCE_DROP = Buffer.alloc(8) @@ -144,7 +146,7 @@ export default class Blockchain implements BlockchainInterface { * * Always keep at least one item on the stack */ - private _cliqueLatestSignerStates?: CliqueLatestSignerStates + private _cliqueLatestSignerStates: CliqueLatestSignerStates = [] /** * List with the latest signer votes @@ -289,16 +291,18 @@ export default class Blockchain implements BlockchainInterface { // Clique: save initial genesis block signers to DB if (this._common.consensusAlgorithm() === 'clique') { - const signers = genesisBlock.header.cliqueEpochTransitionSigners() - dbOps.push(DBOp.set(DBTarget.CliqueSigners, rlp.encode(signers))) + const genesisSignerState = [ + genesisBlock.header.number.toBuffer(), + genesisBlock.header.cliqueEpochTransitionSigners(), + ] + await this.cliqueUpdateSignerStates(genesisSignerState as CliqueSignerState) } await this.dbManager.batch(dbOps) } // Clique: read current signer list if (this._common.consensusAlgorithm() === 'clique') { - const signersB = await this.dbManager.getCliqueSigners() - this._cliqueLatestSignerStates = (rlp.decode(signersB)) + this._cliqueLatestSignerStates = await this.dbManager.getCliqueLatestSignerStates() } // At this point, we can safely set genesisHash as the _genesis hash in this @@ -369,6 +373,30 @@ export default class Blockchain implements BlockchainInterface { } } + private _checkClique() { + if (this._common.consensusAlgorithm() !== 'clique') { + throw new Error('Function call only supported for clique PoA networks') + } + } + + private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) { + const dbOps: DBOp[] = [] + if (signerState) { + this._cliqueLatestSignerStates.push(signerState) + dbOps.push(DBOp.set(DBTarget.CliqueSignerStates, rlp.encode(this._cliqueLatestSignerStates))) + } + await this.dbManager.batch(dbOps) + } + + /** + * Returns a list with the current block signers + * (only clique PoA, throws otherwise) + */ + public cliqueActiveSigners() { + this._checkClique() + return this._cliqueLatestSignerStates[this._cliqueLatestSignerStates.length - 1][1] + } + /** * Returns the specified iterator head. * diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.ts index 863abc3a3e..aa6e37c77c 100644 --- a/packages/blockchain/test/clique.ts +++ b/packages/blockchain/test/clique.ts @@ -6,7 +6,15 @@ tape('Clique', (t) => { t.test('should initialize a clique blockchain', async (st) => { const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) const blockchain = new Blockchain({ common, validateConsensus: false }) - await blockchain.getHead() + + const head = await blockchain.getHead() + st.equals(head.hash().toString('hex'), common.genesis().hash.slice(2), 'correct genesis hash') + + st.deepEquals( + blockchain.cliqueActiveSigners(), + head.cliqueEpochTransitionSigners(), + 'correct genesis signers' + ) st.end() }) }) From 396c8967248a9403a68a495ef110a48fb47a0a1a Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Mon, 11 Jan 2021 11:54:07 +0100 Subject: [PATCH 14/50] blockchain -> clique: added block signature verification and activated consensus validation for PoA clique chains --- packages/block/src/header.ts | 16 +++++++-------- packages/blockchain/src/index.ts | 32 +++++++++++++++++++++--------- packages/blockchain/test/clique.ts | 2 +- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 3b917d209e..da63fcdcc9 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -597,7 +597,7 @@ export class BlockHeader { * * DRAFT: METHOD IN DRAFT STATE, NEEDS THOROUGH TESTING */ - cliqueVerifySignature(signerList: Buffer[]): void { + cliqueVerifySignature(signerList: Buffer[]): boolean { this._checkClique() const extraSeal = this.cliqueExtraSeal() const r = extraSeal.slice(0, 32) @@ -605,15 +605,13 @@ export class BlockHeader { const v = bufferToInt(extraSeal.slice(64, 65)) const pubKey = ecrecover(this.hash(), v, r, s) const address = Address.fromPublicKey(pubKey) - if ( - !signerList.find((signer) => { - return signer.equals(address.toBuffer()) - }) - ) { - throw new Error( - `block signature validation failed (clique), ${address.toString()} not in signer list` - ) + const signerFound = signerList.find((signer) => { + return signer.equals(address.toBuffer()) + }) + if (signerFound) { + return true } + return false } /** diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index e665aadfc4..51c725d4d7 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -74,7 +74,9 @@ export interface BlockchainOptions { * This flags indicates if a block should be validated along the consensus algorithm * or protocol used by the chain, e.g. by verifying the PoW on the block. * - * Supported: 'pow' with 'ethash' algorithm (taken from the `Common` instance) + * Supported consensus types and algorithms (taken from the `Common` instance): + * - 'pow' with 'ethash' algorithm (validates the proof-of-work) + * - 'poa' with 'clique' algorithm (verifies the block signatures) * Default: `true`. */ validateConsensus?: boolean @@ -195,14 +197,20 @@ export default class Blockchain implements BlockchainInterface { this.dbManager = new DBManager(this.db, this._common) if (this._validateConsensus) { - if (this._common.consensusType() !== 'pow') { - throw new Error('consensus validation only supported for pow chains') + if (this._common.consensusType() === 'pow') { + if (this._common.consensusAlgorithm() !== 'ethash') { + throw new Error('consensus validation only supported for pow ethash algorithm') + } else { + this._ethash = new Ethash(this.db) + } } - if (this._common.consensusAlgorithm() !== 'ethash') { - throw new Error('consensus validation only supported for pow ethash algorithm') + if (this._common.consensusType() === 'poa') { + if (this._common.consensusAlgorithm() !== 'clique') { + throw new Error( + 'consensus (signature) validation only supported for poa clique algorithm' + ) + } } - - this._ethash = new Ethash(this.db) } this._heads = {} @@ -541,12 +549,18 @@ export default class Blockchain implements BlockchainInterface { } if (this._validateConsensus) { - if (this._ethash) { - const valid = await this._ethash.verifyPOW(block) + if (this._common.consensusAlgorithm() !== 'ethash') { + const valid = await this._ethash!.verifyPOW(block) if (!valid) { throw new Error('invalid POW') } } + if (this._common.consensusAlgorithm() !== 'clique') { + const valid = header.cliqueVerifySignature(this.cliqueActiveSigners()) + if (!valid) { + throw new Error('invalid PoA block signature (clique)') + } + } } // set total difficulty in the current context scope diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.ts index aa6e37c77c..6582090655 100644 --- a/packages/blockchain/test/clique.ts +++ b/packages/blockchain/test/clique.ts @@ -5,7 +5,7 @@ import Blockchain from '../src' tape('Clique', (t) => { t.test('should initialize a clique blockchain', async (st) => { const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) - const blockchain = new Blockchain({ common, validateConsensus: false }) + const blockchain = new Blockchain({ common }) const head = await blockchain.getHead() st.equals(head.hash().toString('hex'), common.genesis().hash.slice(2), 'correct genesis hash') From b1bf36388cea53de321b06dcf9461bfd0ebf174d Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Mon, 11 Jan 2021 17:33:47 +0100 Subject: [PATCH 15/50] blockchain, block: added basic voting structure, voting test setup from EIP-225, added BlockHeader.cliqueSignatureToAddress() method --- packages/block/src/header.ts | 36 +++--- packages/blockchain/src/db/constants.ts | 6 + packages/blockchain/src/db/manager.ts | 10 +- packages/blockchain/src/db/operation.ts | 6 + packages/blockchain/src/index.ts | 36 ++++-- packages/blockchain/test/clique.ts | 146 +++++++++++++++++++++++- 6 files changed, 217 insertions(+), 23 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index da63fcdcc9..b8e7643a92 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -529,9 +529,9 @@ export class BlockHeader { return this.number.isZero() } - private _checkClique() { + private _checkClique(name: string) { if (this._common.consensusAlgorithm() !== 'clique') { - throw new Error('Function call only supported for clique PoA networks') + throw new Error(`BlockHeader.${name}() call only supported for clique PoA networks`) } } @@ -540,7 +540,7 @@ export class BlockHeader { * header (only clique PoA, throws otherwise) */ cliqueIsEpochTransition(): boolean { - this._checkClique() + this._checkClique('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 @@ -552,7 +552,7 @@ export class BlockHeader { * (only clique PoA, throws otherwise) */ cliqueExtraVanity(): Buffer { - this._checkClique() + this._checkClique('cliqueExtraVanity') return this.extraData.slice(0, CLIQUE_EXTRA_VANITY) } @@ -561,7 +561,7 @@ export class BlockHeader { * (only clique PoA, throws otherwise) */ cliqueExtraSeal(): Buffer { - this._checkClique() + this._checkClique('cliqueExtraSeal') return this.extraData.slice(-CLIQUE_EXTRA_SEAL) } @@ -574,7 +574,7 @@ export class BlockHeader { * in conjunction with `cliqueIsEpochTransition()` */ cliqueEpochTransitionSigners(): Buffer[] { - this._checkClique() + this._checkClique('cliqueEpochTransitionSigners') if (!this.cliqueIsEpochTransition()) { throw new Error('Singers are only included in epoch transition blocks (clique)') } @@ -598,15 +598,10 @@ export class BlockHeader { * DRAFT: METHOD IN DRAFT STATE, NEEDS THOROUGH TESTING */ cliqueVerifySignature(signerList: Buffer[]): boolean { - this._checkClique() - const extraSeal = this.cliqueExtraSeal() - const r = extraSeal.slice(0, 32) - const s = extraSeal.slice(32, 64) - const v = bufferToInt(extraSeal.slice(64, 65)) - const pubKey = ecrecover(this.hash(), v, r, s) - const address = Address.fromPublicKey(pubKey) + this._checkClique('cliqueVerifySignature') + const signerAddress = this.cliqueSignatureToAddress() const signerFound = signerList.find((signer) => { - return signer.equals(address.toBuffer()) + return signer.equals(signerAddress.toBuffer()) }) if (signerFound) { return true @@ -614,6 +609,19 @@ export class BlockHeader { return false } + /** + * Returns the signer address + */ + cliqueSignatureToAddress(): Address { + this._checkClique('cliqueSignatureToAddress') + const extraSeal = this.cliqueExtraSeal() + const r = extraSeal.slice(0, 32) + const s = extraSeal.slice(32, 64) + const v = bufferToInt(extraSeal.slice(64, 65)) + const pubKey = ecrecover(this.hash(), v, r, s) + return Address.fromPublicKey(pubKey) + } + /** * Returns the rlp encoding of the block header. */ diff --git a/packages/blockchain/src/db/constants.ts b/packages/blockchain/src/db/constants.ts index 59a06e258e..a12bedd89b 100644 --- a/packages/blockchain/src/db/constants.ts +++ b/packages/blockchain/src/db/constants.ts @@ -19,6 +19,11 @@ const HEAD_BLOCK_KEY = 'LastBlock' */ const CLIQUE_SIGNERS_KEY = 'CliqueSigners' +/** + * Clique votes + */ +const CLIQUE_VOTES_KEY = 'CliqueVotes' + /** * headerPrefix + number + hash -> header */ @@ -69,6 +74,7 @@ export { HEAD_HEADER_KEY, HEAD_BLOCK_KEY, CLIQUE_SIGNERS_KEY, + CLIQUE_VOTES_KEY, bufBE8, tdKey, headerKey, diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index 3c9186d12c..b56fc2c776 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -12,7 +12,7 @@ import Cache from './cache' import { DatabaseKey, DBOp, DBTarget, DBOpData } from './operation' import type { LevelUp } from 'levelup' -import { CliqueLatestSignerStates } from '..' +import { CliqueLatestSignerStates, CliqueLatestVotes } from '..' const level = require('level-mem') @@ -82,6 +82,14 @@ export class DBManager { return (rlp.decode(signerStates)) } + /** + * Fetches clique votes. + */ + async getCliqueLatestVotes(): Promise { + const votes = await this.get(DBTarget.CliqueVotes) + return (rlp.decode(votes)) + } + /** * Fetches a block (header and body) given a block id, * which can be either its hash or its number. diff --git a/packages/blockchain/src/db/operation.ts b/packages/blockchain/src/db/operation.ts index ddbcebfa82..e518bea964 100644 --- a/packages/blockchain/src/db/operation.ts +++ b/packages/blockchain/src/db/operation.ts @@ -9,6 +9,7 @@ import { numberToHashKey, hashToNumberKey, CLIQUE_SIGNERS_KEY as CLIQUE_SIGNER_STATES_KEY, + CLIQUE_VOTES_KEY, } from './constants' import { CacheMap } from './manager' @@ -21,6 +22,7 @@ export enum DBTarget { NumberToHash, TotalDifficulty, CliqueSignerStates, + CliqueVotes, Body, Header, } @@ -78,6 +80,10 @@ export class DBOp { this.baseDBOp.key = CLIQUE_SIGNER_STATES_KEY break } + case DBTarget.CliqueVotes: { + this.baseDBOp.key = CLIQUE_VOTES_KEY + break + } case DBTarget.HashToNumber: { this.baseDBOp.key = hashToNumberKey(key!.blockHash!) this.cacheString = 'hashToNumber' diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 51c725d4d7..7108adc102 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -12,11 +12,12 @@ const level = require('level-mem') export type CliqueSignerState = [Buffer, Buffer[]] export type CliqueLatestSignerStates = CliqueSignerState[] -export type CliqueVotes = [Buffer, [Buffer, Buffer, Buffer]] -export type CliqueLatestVotes = CliqueVotes[] +export type CliqueVote = [Buffer, [Buffer, Buffer, Buffer]] +export type CliqueLatestVotes = CliqueVote[] -// const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') -// const CLIQUE_NONCE_DROP = Buffer.alloc(8) +const CLIQUE_EMPTY_BENEFICIARY = Buffer.alloc(20) +//const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') +//const CLIQUE_NONCE_DROP = Buffer.alloc(8) type OnBlock = (block: Block, reorg: boolean) => Promise | void @@ -164,7 +165,7 @@ export default class Blockchain implements BlockchainInterface { * * On reorgs delete the top elements from the array until BLOCK_NUMBER > REORG_BLOCK. */ - private _cliqueLatestVotes?: CliqueLatestVotes + private _cliqueLatestVotes: CliqueLatestVotes = [] /** * Creates new Blockchain object @@ -304,6 +305,7 @@ export default class Blockchain implements BlockchainInterface { genesisBlock.header.cliqueEpochTransitionSigners(), ] await this.cliqueUpdateSignerStates(genesisSignerState as CliqueSignerState) + await this.cliqueUpdateVotes() } await this.dbManager.batch(dbOps) } @@ -311,6 +313,7 @@ export default class Blockchain implements BlockchainInterface { // Clique: read current signer list if (this._common.consensusAlgorithm() === 'clique') { this._cliqueLatestSignerStates = await this.dbManager.getCliqueLatestSignerStates() + this._cliqueLatestVotes = await this.dbManager.getCliqueLatestVotes() } // At this point, we can safely set genesisHash as the _genesis hash in this @@ -396,6 +399,20 @@ export default class Blockchain implements BlockchainInterface { await this.dbManager.batch(dbOps) } + private async cliqueUpdateVotes(header?: BlockHeader) { + // Block contains a vote on a new signer + if (header && !header.coinbase.toBuffer().equals(CLIQUE_EMPTY_BENEFICIARY)) { + const latestVote = [ + header.number.toBuffer(), + [header.cliqueSignatureToAddress().toBuffer(), header.coinbase.toBuffer(), header.nonce], + ] + this._cliqueLatestVotes.push(latestVote as CliqueVote) + } + const dbOps: DBOp[] = [] + dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(this._cliqueLatestVotes))) + await this.dbManager.batch(dbOps) + } + /** * Returns a list with the current block signers * (only clique PoA, throws otherwise) @@ -549,13 +566,13 @@ export default class Blockchain implements BlockchainInterface { } if (this._validateConsensus) { - if (this._common.consensusAlgorithm() !== 'ethash') { + if (this._common.consensusAlgorithm() === 'ethash') { const valid = await this._ethash!.verifyPOW(block) if (!valid) { throw new Error('invalid POW') } } - if (this._common.consensusAlgorithm() !== 'clique') { + if (this._common.consensusAlgorithm() === 'clique') { const valid = header.cliqueVerifySignature(this.cliqueActiveSigners()) if (!valid) { throw new Error('invalid PoA block signature (clique)') @@ -593,6 +610,11 @@ export default class Blockchain implements BlockchainInterface { this._genesis = blockHash } + // Clique: update signer votes and state + if (this._common.consensusAlgorithm() === 'clique') { + await this.cliqueUpdateVotes(header) + } + // delete higher number assignments and overwrite stale canonical chain await this._deleteCanonicalChainReferences(blockNumber.addn(1), blockHash, dbOps) // from the current header block, check the blockchain in reverse (i.e. diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.ts index 6582090655..6b7805966c 100644 --- a/packages/blockchain/test/clique.ts +++ b/packages/blockchain/test/clique.ts @@ -1,8 +1,10 @@ +import { Block } from '@ethereumjs/block' import Common from '@ethereumjs/common' +import { intToBuffer, ecsign } from 'ethereumjs-util' import tape from 'tape' import Blockchain from '../src' -tape('Clique', (t) => { +tape('Clique: Initialization', (t) => { t.test('should initialize a clique blockchain', async (st) => { const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) const blockchain = new Blockchain({ common }) @@ -17,4 +19,146 @@ tape('Clique', (t) => { ) st.end() }) + + const COMMON = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) + const EXTRA_DATA = Buffer.alloc(97) + const GAS_LIMIT = 8000000 + + type Signer = { + address: Buffer + privateKey: Buffer + publicKey: Buffer + } + + const A: Signer = { + address: Buffer.from('0b90087d864e82a284dca15923f3776de6bb016f', 'hex'), + privateKey: Buffer.from( + '64bf9cc30328b0e42387b3c82c614e6386259136235e20c1357bd11cdee86993', + 'hex' + ), + publicKey: Buffer.from( + '40b2ebdf4b53206d2d3d3d59e7e2f13b1ea68305aec71d5d24cefe7f24ecae886d241f9267f04702d7f693655eb7b4aa23f30dcd0c3c5f2b970aad7c8a828195', + 'hex' + ), + } + + const B: Signer = { + address: Buffer.from('dc7bc81ddf67d037d7439f8e6ff12f3d2a100f71', 'hex'), + privateKey: Buffer.from( + '86b0ff7b6cf70786f29f297c57562905ab0b6c32d69e177a46491e56da9e486e', + 'hex' + ), + publicKey: Buffer.from( + 'd3e3d2b722e325bfc085ff5638a112b4e7e88ff13f92fc7f6cfc14b5a25e8d1545a2f27d8537b96e8919949d5f8c139ae7fc81aea7cf7fe5d43d7faaa038e35b', + 'hex' + ), + } + + const C: Signer = { + address: Buffer.from('8458f408106c4875c96679f3f556a511beabe138', 'hex'), + privateKey: Buffer.from( + '159e95d07a6c64ddbafa6036cdb7b8114e6e8cdc449ca4b0468a6d0c955f991b', + 'hex' + ), + publicKey: Buffer.from( + 'f02724341e2df54cf53515f079b1354fa8d437e79c5b091b8d8cc7cbcca00fd8ad854cb3b3a85b06c44ecb7269404a67be88b561f2224c94d133e5fc21be915c', + 'hex' + ), + } + + /*const D: Signer = { + address: Buffer.from('83c30730d1972baa09765a1ac72a43db27fedce5', 'hex'), + privateKey: Buffer.from( + 'f216ddcf276079043c52b5dd144aa073e6b272ad4bfeaf4fbbc044aa478d1927', + 'hex' + ), + publicKey: Buffer.from( + '555b19a5cbe6dd082a4a1e1e0520dd52a82ba24fd5598ea31f0f31666c40905ed319314c5fb06d887b760229e1c0e616294e7b1cb5dfefb71507c9112132ce56', + 'hex' + ), + }*/ + + const NONCE_AUTH = Buffer.from('ffffffffffffffff', 'hex') + const NONCE_DROP = Buffer.from('0000000000000000', 'hex') + + const initWithSigners = (signers: Buffer[]) => { + const blocks: Block[] = [] + + const extraData = Buffer.concat([Buffer.alloc(32), ...signers, Buffer.alloc(65)]) + const genesisBlock = Block.genesis( + { header: { gasLimit: GAS_LIMIT, extraData } }, + { common: COMMON } + ) + blocks.push(genesisBlock) + + const blockchain = new Blockchain({ + validateBlocks: true, + validateConsensus: false, + genesisBlock, + common: COMMON, + }) + return { blocks, blockchain } + } + + const addNextBlock = async ( + blockchain: Blockchain, + blocks: Block[], + signer: Signer, + beneficiary?: [Signer, boolean] + ) => { + const number = blocks.length + const lastBlock = blocks[number - 1] + + let coinbase = Buffer.alloc(20) + let nonce = NONCE_DROP + if (beneficiary) { + coinbase = beneficiary[0].address + if (beneficiary[1]) { + nonce = NONCE_AUTH + } + } + + const blockData = { + header: { + number, + parentHash: lastBlock.hash(), + coinbase, + timestamp: lastBlock.header.timestamp.addn(15), + extraData: EXTRA_DATA, + gasLimit: GAS_LIMIT, + nonce, + }, + } + const block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) + const signature = ecsign(block.header.hash(), signer.privateKey) + const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v)]) + //@ts-ignore + block.header.extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) + + await blockchain.putBlock(block) + blocks.push(block) + return block + } + + // Test Cases: https://eips.ethereum.org/EIPS/eip-225 + t.test('Clique Voting: Single signer, no votes cast', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address]) + const block = await addNextBlock(blockchain, blocks, A) + st.equal(block.header.number.toNumber(), 1) + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address]) + st.end() + }) + + t.test('Clique Voting: Single signer, voting to add two others', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address]) + await addNextBlock(blockchain, blocks, A, [B, true]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A, [C, true]) + st.deepEqual( + blockchain.cliqueActiveSigners(), + [A.address, B.address], + 'only accept first two, third needs 3 votes already' + ) + st.end() + }) }) From 8b82e3a6c99cd9239d6852e4b584aa569d7294c9 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Mon, 11 Jan 2021 18:27:38 +0100 Subject: [PATCH 16/50] blockchain -> clique: do not check total difficulty on poa chain when adding new blocks or headers --- packages/blockchain/src/index.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 7108adc102..8d27fb0f3e 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -402,10 +402,19 @@ export default class Blockchain implements BlockchainInterface { private async cliqueUpdateVotes(header?: BlockHeader) { // Block contains a vote on a new signer if (header && !header.coinbase.toBuffer().equals(CLIQUE_EMPTY_BENEFICIARY)) { - const latestVote = [ - header.number.toBuffer(), - [header.cliqueSignatureToAddress().toBuffer(), header.coinbase.toBuffer(), header.nonce], - ] + const signer = header.cliqueSignatureToAddress().toBuffer() + const beneficiary = header.coinbase.toBuffer() + const nonce = header.nonce + const latestVote = [header.number.toBuffer(), [signer, beneficiary, header.nonce]] + const alreadyVoted = this._cliqueLatestVotes.find((vote: CliqueVote) => { + vote[1][0].equals(signer) && vote[1][1].equals(beneficiary) && vote[1][2].equals(nonce) + }) + ? true + : false + if (!alreadyVoted) { + //TODO do the voting + } + this._cliqueLatestVotes.push(latestVote as CliqueVote) } const dbOps: DBOp[] = [] @@ -599,7 +608,11 @@ export default class Blockchain implements BlockchainInterface { dbOps = dbOps.concat(DBSetBlockOrHeader(block)) // if total difficulty is higher than current, add it to canonical chain - if (block.isGenesis() || td.gt(currentTd.header)) { + if ( + block.isGenesis() || + (this._common.consensusType() === 'pow' && td.gt(currentTd.header)) || + this._common.consensusType() === 'poa' + ) { this._headHeaderHash = blockHash if (item instanceof Block) { this._headBlockHash = blockHash From 31160ac27c313fd7180f865ebce3c706db3e16e2 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Tue, 12 Jan 2021 09:36:37 +0100 Subject: [PATCH 17/50] blockchain -> clique: added signer voting logic --- packages/blockchain/src/index.ts | 37 +++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 8d27fb0f3e..fe930fe1cf 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -16,7 +16,7 @@ export type CliqueVote = [Buffer, [Buffer, Buffer, Buffer]] export type CliqueLatestVotes = CliqueVote[] const CLIQUE_EMPTY_BENEFICIARY = Buffer.alloc(20) -//const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') +const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') //const CLIQUE_NONCE_DROP = Buffer.alloc(8) type OnBlock = (block: Block, reorg: boolean) => Promise | void @@ -402,20 +402,47 @@ export default class Blockchain implements BlockchainInterface { private async cliqueUpdateVotes(header?: BlockHeader) { // Block contains a vote on a new signer if (header && !header.coinbase.toBuffer().equals(CLIQUE_EMPTY_BENEFICIARY)) { + // 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3,... + const SIGNER_LIMIT = Math.floor(this.cliqueActiveSigners().length / 2) + 1 + const signer = header.cliqueSignatureToAddress().toBuffer() const beneficiary = header.coinbase.toBuffer() const nonce = header.nonce - const latestVote = [header.number.toBuffer(), [signer, beneficiary, header.nonce]] + const latestVote = [header.number.toBuffer(), [signer, beneficiary, nonce]] + + // Always add the latest vote to the history no matter if already voted + // the same vote or not + this._cliqueLatestVotes.push(latestVote as CliqueVote) + const alreadyVoted = this._cliqueLatestVotes.find((vote: CliqueVote) => { vote[1][0].equals(signer) && vote[1][1].equals(beneficiary) && vote[1][2].equals(nonce) }) ? true : false + // If same vote not already in history see if there is a new majority consensus + // to update the signer list if (!alreadyVoted) { - //TODO do the voting + const beneficiaryVotes = this._cliqueLatestVotes.filter((vote: CliqueVote) => { + return vote[1][1].equals(beneficiary) && vote[1][2].equals(nonce) + }) + const benficiaryVoteNum = beneficiaryVotes.length + // Majority consensus + if (benficiaryVoteNum >= SIGNER_LIMIT) { + let activeSigners = this.cliqueActiveSigners() + // Authorize new signer + if (nonce.equals(CLIQUE_NONCE_AUTH)) { + activeSigners.push(beneficiary) + // Drop existing signer + } else { + // nonce equals CLIQUE_NONCE_DROP + activeSigners = activeSigners.filter((signer: Buffer) => { + return !signer.equals(beneficiary) + }) + } + const newSignerState: CliqueSignerState = [header.number.toBuffer(), activeSigners] + await this.cliqueUpdateSignerStates(newSignerState! as CliqueSignerState) + } } - - this._cliqueLatestVotes.push(latestVote as CliqueVote) } const dbOps: DBOp[] = [] dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(this._cliqueLatestVotes))) From 222fbfb1d2b750fbf0ed61234db07e1dfbec403d Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Tue, 12 Jan 2021 12:28:37 +0100 Subject: [PATCH 18/50] blockchain, block -> clique: additional voting test cases, v value fix in BlockHeader.cliqueSignatureToAddress() method --- packages/block/src/header.ts | 2 +- packages/blockchain/src/index.ts | 12 +-- packages/blockchain/test/clique.ts | 137 ++++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 9 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index b8e7643a92..5f6cf2e359 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -617,7 +617,7 @@ export class BlockHeader { const extraSeal = this.cliqueExtraSeal() const r = extraSeal.slice(0, 32) const s = extraSeal.slice(32, 64) - const v = bufferToInt(extraSeal.slice(64, 65)) + const v = bufferToInt(extraSeal.slice(64, 65)) + 27 const pubKey = ecrecover(this.hash(), v, r, s) return Address.fromPublicKey(pubKey) } diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index fe930fe1cf..a2899a7c56 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -410,15 +410,17 @@ export default class Blockchain implements BlockchainInterface { const nonce = header.nonce const latestVote = [header.number.toBuffer(), [signer, beneficiary, nonce]] - // Always add the latest vote to the history no matter if already voted - // the same vote or not - this._cliqueLatestVotes.push(latestVote as CliqueVote) - const alreadyVoted = this._cliqueLatestVotes.find((vote: CliqueVote) => { - vote[1][0].equals(signer) && vote[1][1].equals(beneficiary) && vote[1][2].equals(nonce) + return ( + vote[1][0].equals(signer) && vote[1][1].equals(beneficiary) && vote[1][2].equals(nonce) + ) }) ? true : false + + // Always add the latest vote to the history no matter if already voted + // the same vote or not + this._cliqueLatestVotes.push(latestVote as CliqueVote) // If same vote not already in history see if there is a new majority consensus // to update the signer list if (!alreadyVoted) { diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.ts index 6b7805966c..265e7e8791 100644 --- a/packages/blockchain/test/clique.ts +++ b/packages/blockchain/test/clique.ts @@ -66,7 +66,7 @@ tape('Clique: Initialization', (t) => { ), } - /*const D: Signer = { + const D: Signer = { address: Buffer.from('83c30730d1972baa09765a1ac72a43db27fedce5', 'hex'), privateKey: Buffer.from( 'f216ddcf276079043c52b5dd144aa073e6b272ad4bfeaf4fbbc044aa478d1927', @@ -76,7 +76,19 @@ tape('Clique: Initialization', (t) => { '555b19a5cbe6dd082a4a1e1e0520dd52a82ba24fd5598ea31f0f31666c40905ed319314c5fb06d887b760229e1c0e616294e7b1cb5dfefb71507c9112132ce56', 'hex' ), - }*/ + } + + const E: Signer = { + address: Buffer.from('6f62d8382bf2587361db73ceca28be91b2acb6df', 'hex'), + privateKey: Buffer.from( + '2a6e9ad5a6a8e4f17149b8bc7128bf090566a11dbd63c30e5a0ee9f161309cd6', + 'hex' + ), + publicKey: Buffer.from( + 'ca0a55f6e81cb897aee6a1c390aa83435c41048faa0564b226cfc9f3df48b73e846377fb0fd606df073addc7bd851f22547afbbdd5c3b028c91399df802083a2', + 'hex' + ), + } const NONCE_AUTH = Buffer.from('ffffffffffffffff', 'hex') const NONCE_DROP = Buffer.from('0000000000000000', 'hex') @@ -131,7 +143,7 @@ tape('Clique: Initialization', (t) => { } const block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) const signature = ecsign(block.header.hash(), signer.privateKey) - const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v)]) + const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) //@ts-ignore block.header.extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) @@ -157,8 +169,127 @@ tape('Clique: Initialization', (t) => { st.deepEqual( blockchain.cliqueActiveSigners(), [A.address, B.address], + 'only accept first, second needs 2 votes' + ) + st.end() + }) + + t.test('Two signers, voting to add three others', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address]) + 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]) + + st.deepEqual( + blockchain.cliqueActiveSigners(), + [A.address, B.address, C.address, D.address], 'only accept first two, third needs 3 votes already' ) st.end() }) + + t.test('Clique Voting: Single signer, dropping itself)', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address]) + await addNextBlock(blockchain, blocks, A, [A, false]) + + st.deepEqual( + blockchain.cliqueActiveSigners(), + [], + 'weird, but one less cornercase by explicitly allowing this' + ) + st.end() + }) + + t.test( + 'Clique Voting: Two signers, actually needing mutual consent to drop either of them', + async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address]) + await addNextBlock(blockchain, blocks, A, [B, false]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], 'not fulfilled') + st.end() + } + ) + + t.test( + 'Clique Voting: Two signers, actually needing mutual consent to drop either of them', + async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address]) + await addNextBlock(blockchain, blocks, A, [B, false]) + await addNextBlock(blockchain, blocks, B, [B, false]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address], 'fulfilled') + st.end() + } + ) + + t.test('Clique Voting: Three signers, two of them deciding to drop the third', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B, [C, false]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') + st.end() + }) + + t.test( + 'Clique Voting: Four signers, consensus of two not being enough to drop anyone', + async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B, [C, false]) + + st.deepEqual( + blockchain.cliqueActiveSigners(), + [A.address, B.address, C.address, D.address], + '' + ) + st.end() + } + ) + + t.test( + 'Clique Voting: Four signers, consensus of three already being enough to drop someone', + async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + await addNextBlock(blockchain, blocks, A, [D, false]) + await addNextBlock(blockchain, blocks, B, [D, false]) + await addNextBlock(blockchain, blocks, C, [D, false]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address, C.address], '') + st.end() + } + ) + + t.test('Clique Voting: Authorizations are counted once per signer per target', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address]) + await addNextBlock(blockchain, blocks, A, [C, true]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A, [C, true]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A, [C, true]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') + st.end() + }) + + t.test('Clique Voting: ', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address]) + await addNextBlock(blockchain, blocks, A, [B, true]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') + st.end() + }) + + t.test('Clique Voting: ', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address]) + await addNextBlock(blockchain, blocks, A, [B, true]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') + st.end() + }) }) From b6b8bed1c738f8d5d555e4a5353dd19952bfde38 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Tue, 12 Jan 2021 17:36:20 +0100 Subject: [PATCH 19/50] block -> clique: fixed BlockHeader.hash() function, moved clique tests to dedicated own file, remove extraData readonly property for easier signature addition --- packages/block/src/header.ts | 16 ++--- packages/block/test/clique.spec.ts | 99 ++++++++++++++++++++++++++++++ packages/block/test/header.spec.ts | 54 ---------------- packages/blockchain/test/clique.ts | 1 - 4 files changed, 107 insertions(+), 63 deletions(-) create mode 100644 packages/block/test/clique.spec.ts diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 5f6cf2e359..a004b0f993 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -36,7 +36,8 @@ export class BlockHeader { public readonly gasLimit: BN public readonly gasUsed: BN public readonly timestamp: BN - public readonly extraData: Buffer + // no readonly for extraData to ease signature addition for PoA blocks + public extraData: Buffer public readonly mixHash: Buffer public readonly nonce: Buffer @@ -515,11 +516,10 @@ export class BlockHeader { hash(): Buffer { const raw = this.raw() // Hash for PoA clique blocks is created without the seal - if (this._common.consensusAlgorithm() === 'clique') { + if (this._common.consensusAlgorithm() === 'clique' && !this.isGenesis()) { raw[12] = this.extraData.slice(0, this.extraData.length - CLIQUE_EXTRA_SEAL) } - - return rlphash(this.raw()) + return rlphash(raw) } /** @@ -578,8 +578,10 @@ export class BlockHeader { if (!this.cliqueIsEpochTransition()) { throw new Error('Singers are only included in epoch transition blocks (clique)') } - let signerBuffer = this.extraData.slice(CLIQUE_EXTRA_VANITY) - signerBuffer = signerBuffer.slice(0, signerBuffer.length - CLIQUE_EXTRA_SEAL) + + 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 @@ -594,8 +596,6 @@ export class BlockHeader { * (only clique PoA, throws otherwise) * * Method throws if signature is invalid - * - * DRAFT: METHOD IN DRAFT STATE, NEEDS THOROUGH TESTING */ cliqueVerifySignature(signerList: Buffer[]): boolean { this._checkClique('cliqueVerifySignature') diff --git a/packages/block/test/clique.spec.ts b/packages/block/test/clique.spec.ts new file mode 100644 index 0000000000..66aaa98c74 --- /dev/null +++ b/packages/block/test/clique.spec.ts @@ -0,0 +1,99 @@ +import tape from 'tape' +import Common from '@ethereumjs/common' +import { BlockHeader } from '../src/header' +import { ecsign, intToBuffer } from 'ethereumjs-util' + +tape('[Header]: Clique PoA Functionality', function (t) { + const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) + + t.test('Header Data', function (st) { + let header = BlockHeader.fromHeaderData({ number: 1 }) + st.throws(() => { + header.cliqueIsEpochTransition() + }, 'cliqueIsEpochTransition() -> should throw on PoW networks') + + header = BlockHeader.genesis({}, { common }) + st.ok( + header.cliqueIsEpochTransition(), + 'cliqueIsEpochTransition() -> should indicate an epoch transition for the genesis block' + ) + + header = BlockHeader.fromHeaderData({ number: 1, extraData: Buffer.alloc(97) }, { common }) + st.notOk( + header.cliqueIsEpochTransition(), + 'cliqueIsEpochTransition() -> should correctly identify a non-epoch block' + ) + st.deepEqual( + header.cliqueExtraVanity(), + Buffer.alloc(32), + 'cliqueExtraVanity() -> should return correct extra vanity value' + ) + st.deepEqual( + header.cliqueExtraSeal(), + Buffer.alloc(65), + 'cliqueExtraSeal() -> should return correct extra seal value' + ) + st.throws(() => { + header.cliqueEpochTransitionSigners() + }, 'cliqueEpochTransitionSigners() -> should throw on non-epch block') + + header = BlockHeader.fromHeaderData({ number: 60000, extraData: Buffer.alloc(137) }, { common }) + st.ok( + header.cliqueIsEpochTransition(), + 'cliqueIsEpochTransition() -> should correctly identify an epoch block' + ) + st.deepEqual( + header.cliqueExtraVanity(), + Buffer.alloc(32), + 'cliqueExtraVanity() -> should return correct extra vanity value' + ) + st.deepEqual( + header.cliqueExtraSeal(), + Buffer.alloc(65), + 'cliqueExtraSeal() -> should return correct extra seal value' + ) + const msg = + 'cliqueEpochTransitionSigners() -> should return the correct epoch transition signer list on epoch block' + st.deepEqual(header.cliqueEpochTransitionSigners(), [Buffer.alloc(20), Buffer.alloc(20)], msg) + + st.end() + }) + + type Signer = { + address: Buffer + privateKey: Buffer + publicKey: Buffer + } + + const A: Signer = { + address: Buffer.from('0b90087d864e82a284dca15923f3776de6bb016f', 'hex'), + privateKey: Buffer.from( + '64bf9cc30328b0e42387b3c82c614e6386259136235e20c1357bd11cdee86993', + 'hex' + ), + publicKey: Buffer.from( + '40b2ebdf4b53206d2d3d3d59e7e2f13b1ea68305aec71d5d24cefe7f24ecae886d241f9267f04702d7f693655eb7b4aa23f30dcd0c3c5f2b970aad7c8a828195', + 'hex' + ), + } + + t.test('Signing', function (st) { + const header = BlockHeader.fromHeaderData( + { number: 1, extraData: Buffer.alloc(97) }, + { common, freeze: false } + ) + const signature = ecsign(header.hash(), A.privateKey) + const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) + header.extraData = Buffer.concat([header.cliqueExtraVanity(), signatureB]) + + st.equal(header.extraData.length, 97) + st.deepEqual(header.cliqueVerifySignature([A.address]), true, 'should verify signature') + st.deepEqual( + header.cliqueSignatureToAddress().toBuffer(), + A.address, + 'should recover the correct signer address' + ) + + st.end() + }) +}) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 3a1de0635a..8531150833 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -145,60 +145,6 @@ tape('[Block]: Header functions', function (t) { st.end() }) - t.test('should test clique functionality', function (st) { - let header = BlockHeader.fromHeaderData({ number: 1 }) - st.throws(() => { - header.cliqueIsEpochTransition() - }, 'cliqueIsEpochTransition() -> should throw on PoW networks') - - const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) - header = BlockHeader.genesis({}, { common }) - st.ok( - header.cliqueIsEpochTransition(), - 'cliqueIsEpochTransition() -> should indicate an epoch transition for the genesis block' - ) - - header = BlockHeader.fromHeaderData({ number: 1, extraData: Buffer.alloc(97) }, { common }) - st.notOk( - header.cliqueIsEpochTransition(), - 'cliqueIsEpochTransition() -> should correctly identify a non-epoch block' - ) - st.deepEqual( - header.cliqueExtraVanity(), - Buffer.alloc(32), - 'cliqueExtraVanity() -> should return correct extra vanity value' - ) - st.deepEqual( - header.cliqueExtraSeal(), - Buffer.alloc(65), - 'cliqueExtraSeal() -> should return correct extra seal value' - ) - st.throws(() => { - header.cliqueEpochTransitionSigners() - }, 'cliqueEpochTransitionSigners() -> should throw on non-epch block') - - header = BlockHeader.fromHeaderData({ number: 60000, extraData: Buffer.alloc(137) }, { common }) - st.ok( - header.cliqueIsEpochTransition(), - 'cliqueIsEpochTransition() -> should correctly identify an epoch block' - ) - st.deepEqual( - header.cliqueExtraVanity(), - Buffer.alloc(32), - 'cliqueExtraVanity() -> should return correct extra vanity value' - ) - st.deepEqual( - header.cliqueExtraSeal(), - Buffer.alloc(65), - 'cliqueExtraSeal() -> should return correct extra seal value' - ) - const msg = - 'cliqueEpochTransitionSigners() -> should return the correct epoch transition signer list on epoch block' - st.deepEqual(header.cliqueEpochTransitionSigners(), [Buffer.alloc(20), Buffer.alloc(20)], msg) - - st.end() - }) - t.test('should test genesis hashes (mainnet default)', function (st) { const testDataGenesis = require('./testdata/genesishashestest.json').test const header = BlockHeader.genesis() diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.ts index 265e7e8791..c486531c46 100644 --- a/packages/blockchain/test/clique.ts +++ b/packages/blockchain/test/clique.ts @@ -144,7 +144,6 @@ tape('Clique: Initialization', (t) => { const block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) const signature = ecsign(block.header.hash(), signer.privateKey) const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) - //@ts-ignore block.header.extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) await blockchain.putBlock(block) From f9c615740070b93d15a770f86859cf6817308a09 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Tue, 12 Jan 2021 18:16:58 +0100 Subject: [PATCH 20/50] blockchain -> clique: reset votes on epoch transition blocks, added more testcases --- packages/blockchain/src/index.ts | 16 ++++- packages/blockchain/test/clique.ts | 102 +++++++++++++++++++++++++---- 2 files changed, 104 insertions(+), 14 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index a2899a7c56..8b2b1bae3c 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -440,6 +440,10 @@ export default class Blockchain implements BlockchainInterface { activeSigners = activeSigners.filter((signer: Buffer) => { return !signer.equals(beneficiary) }) + // Discard votes from removed signer + this._cliqueLatestVotes = this._cliqueLatestVotes.filter((vote: CliqueVote) => { + return !vote[1][0].equals(beneficiary) + }) } const newSignerState: CliqueSignerState = [header.number.toBuffer(), activeSigners] await this.cliqueUpdateSignerStates(newSignerState! as CliqueSignerState) @@ -654,7 +658,17 @@ export default class Blockchain implements BlockchainInterface { // Clique: update signer votes and state if (this._common.consensusAlgorithm() === 'clique') { - await this.cliqueUpdateVotes(header) + // Reset all votes on epoch transition blocks + if (header.cliqueIsEpochTransition()) { + this._cliqueLatestVotes = [] + await this.cliqueUpdateVotes() + // TODO: on epoch transition blocks we might want to validate the + // calculated active signer list towards the epoch block + // signer checkpoint list in the extraData field + // Add eventual new header vote on non-epoch transition blocks + } else { + await this.cliqueUpdateVotes(header) + } } // delete higher number assignments and overwrite stale canonical chain diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.ts index c486531c46..cc3b9e7acf 100644 --- a/packages/blockchain/test/clique.ts +++ b/packages/blockchain/test/clique.ts @@ -231,7 +231,7 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) - st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) st.end() }) @@ -242,11 +242,7 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) - st.deepEqual( - blockchain.cliqueActiveSigners(), - [A.address, B.address, C.address, D.address], - '' - ) + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address, C.address, D.address]) st.end() } ) @@ -259,7 +255,7 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B, [D, false]) await addNextBlock(blockchain, blocks, C, [D, false]) - st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address, C.address], '') + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address, C.address]) st.end() } ) @@ -272,23 +268,103 @@ tape('Clique: Initialization', (t) => { await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [C, true]) - st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) st.end() }) - t.test('Clique Voting: ', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address]) - await addNextBlock(blockchain, blocks, A, [B, true]) + t.test('Clique Voting: Authorizing multiple accounts concurrently is permitted', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address]) + await addNextBlock(blockchain, blocks, A, [C, true]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A, [D, true]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [D, true]) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [C, true]) - st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address, D.address, C.address]) + st.end() + }) + + t.test('Clique Voting: Deauthorizations are counted once per signer per target', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address]) + await addNextBlock(blockchain, blocks, A, [B, false]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A, [B, false]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, A, [B, false]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) + st.end() + }) + + t.test('Clique Voting: Deauthorizing multiple accounts concurrently is permitted', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A, [D, false]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [D, false]) + await addNextBlock(blockchain, blocks, C, [D, false]) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [C, false]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) + st.end() + }) + + t.test('Clique Voting: Votes from deauthorized signers are discarded immediately', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address]) + await addNextBlock(blockchain, blocks, C, [B, false]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B, [C, false]) + await addNextBlock(blockchain, blocks, A, [B, false]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], 'deauth votes') + st.end() + }) + + t.test('Clique Voting: Votes from deauthorized signers are discarded immediately', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address]) + await addNextBlock(blockchain, blocks, C, [D, true]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B, [C, false]) + await addNextBlock(blockchain, blocks, A, [D, true]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], 'auth votes') + st.end() + }) + + // TODO: fix test case + /*t.test('Clique Voting: Changes reaching consensus out of bounds (via a deauth) execute on touch', async (st) => { + const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A, [D, false]) + await addNextBlock(blockchain, blocks, B, [C, false]) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [D, false]) + await addNextBlock(blockchain, blocks, C, [D, false]) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, C, [C, true]) + + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) st.end() }) + // TODO: add two additional test cases, further last test cases not relevant yet t.test('Clique Voting: ', async (st) => { const { blocks, blockchain } = initWithSigners([A.address]) await addNextBlock(blockchain, blocks, A, [B, true]) st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') st.end() - }) + })*/ }) From 4828d1ddd649717678ce3bf1d7f89e014cb2070a Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Tue, 12 Jan 2021 18:21:47 +0100 Subject: [PATCH 21/50] vm -> clique: use signer address for the COINBASE opcode for transaction execution on PoA clique networks --- packages/vm/lib/evm/eei.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vm/lib/evm/eei.ts b/packages/vm/lib/evm/eei.ts index 02c79ad38f..bd0a6a35ee 100644 --- a/packages/vm/lib/evm/eei.ts +++ b/packages/vm/lib/evm/eei.ts @@ -259,7 +259,14 @@ export default class EEI { * Returns the block's beneficiary address. */ getBlockCoinbase(): BN { - return new BN(this._env.block.header.coinbase.buf) + let coinbase: BN + if (this._common.consensusAlgorithm() === 'clique') { + coinbase = new BN(this._env.block.header.cliqueSignatureToAddress().toBuffer()) + } else { + coinbase = new BN(this._env.block.header.coinbase.buf) + } + + return coinbase } /** From ee6902c9a1e69cf103d6034293fddcdfbf7220a4 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 14:09:11 -0800 Subject: [PATCH 22/50] only calculate td for ethash consensus --- packages/blockchain/src/index.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 8b2b1bae3c..02cd4a9926 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -622,22 +622,28 @@ export default class Blockchain implements BlockchainInterface { } } - // set total difficulty in the current context scope - if (this._headHeaderHash) { - currentTd.header = await this.getTotalDifficulty(this._headHeaderHash) - } - if (this._headBlockHash) { - currentTd.block = await this.getTotalDifficulty(this._headBlockHash) - } + if (this._common.consensusAlgorithm() === 'ethash') { + // set total difficulty in the current context scope + console.log('gogo1') + if (this._headHeaderHash) { + currentTd.header = await this.getTotalDifficulty(this._headHeaderHash) + } + console.log('gogo2') + if (this._headBlockHash) { + currentTd.block = await this.getTotalDifficulty(this._headBlockHash) + } + console.log('gogo3') - // calculate the total difficulty of the new block - const parentTd = await this.getTotalDifficulty(header.parentHash, blockNumber.subn(1)) - td.iadd(parentTd) + // calculate the total difficulty of the new block + const parentTd = await this.getTotalDifficulty(header.parentHash, blockNumber.subn(1)) + td.iadd(parentTd) + console.log('gogo4') + } - // save block and total difficulty to the database + // save total difficulty to the database dbOps = dbOps.concat(DBSetTD(td, blockNumber, blockHash)) - // save header/block + // save header/block to the database dbOps = dbOps.concat(DBSetBlockOrHeader(block)) // if total difficulty is higher than current, add it to canonical chain From 8bfafd5699d613920625a3e3037ca132cdd2c8cf Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 14:11:06 -0800 Subject: [PATCH 23/50] catch clique db gets to return empty on NotFoundError --- packages/blockchain/src/db/manager.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index b56fc2c776..53619cb8e9 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -78,16 +78,30 @@ export class DBManager { * Fetches clique signers. */ async getCliqueLatestSignerStates(): Promise { - const signerStates = await this.get(DBTarget.CliqueSignerStates) - return (rlp.decode(signerStates)) + try { + const signerStates = await this.get(DBTarget.CliqueSignerStates) + return rlp.decode(signerStates) as CliqueLatestSignerStates + } catch (error) { + if (error.type === 'NotFoundError') { + return [] + } + throw error + } } /** * Fetches clique votes. */ async getCliqueLatestVotes(): Promise { - const votes = await this.get(DBTarget.CliqueVotes) - return (rlp.decode(votes)) + try { + const votes = await this.get(DBTarget.CliqueVotes) + return rlp.decode(votes) as CliqueLatestVotes + } catch (error) { + if (error.type === 'NotFoundError') { + return [] + } + throw error + } } /** From cac505b5fc5e4b9dc6c1992eb80900c29bfb865a Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 14:14:26 -0800 Subject: [PATCH 24/50] remove need for CLIQUE_EMPTY_BENEFICIARY (can use `Address.isZero` instead) use CLIQUE_NONCE_DROP and throw if invalid nonce for clique block --- packages/blockchain/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 02cd4a9926..a6d81a1d87 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -15,9 +15,8 @@ export type CliqueLatestSignerStates = CliqueSignerState[] export type CliqueVote = [Buffer, [Buffer, Buffer, Buffer]] export type CliqueLatestVotes = CliqueVote[] -const CLIQUE_EMPTY_BENEFICIARY = Buffer.alloc(20) const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') -//const CLIQUE_NONCE_DROP = Buffer.alloc(8) +const CLIQUE_NONCE_DROP = Buffer.alloc(8) type OnBlock = (block: Block, reorg: boolean) => Promise | void @@ -401,7 +400,7 @@ export default class Blockchain implements BlockchainInterface { private async cliqueUpdateVotes(header?: BlockHeader) { // Block contains a vote on a new signer - if (header && !header.coinbase.toBuffer().equals(CLIQUE_EMPTY_BENEFICIARY)) { + if (header && !header.coinbase.isZero()) { // 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3,... const SIGNER_LIMIT = Math.floor(this.cliqueActiveSigners().length / 2) + 1 @@ -435,8 +434,7 @@ export default class Blockchain implements BlockchainInterface { if (nonce.equals(CLIQUE_NONCE_AUTH)) { activeSigners.push(beneficiary) // Drop existing signer - } else { - // nonce equals CLIQUE_NONCE_DROP + } else if (nonce.equals(CLIQUE_NONCE_DROP)) { activeSigners = activeSigners.filter((signer: Buffer) => { return !signer.equals(beneficiary) }) @@ -444,6 +442,8 @@ export default class Blockchain implements BlockchainInterface { this._cliqueLatestVotes = this._cliqueLatestVotes.filter((vote: CliqueVote) => { return !vote[1][0].equals(beneficiary) }) + } else { + throw new Error('Invalid nonce for clique block: ' + nonce) } const newSignerState: CliqueSignerState = [header.number.toBuffer(), activeSigners] await this.cliqueUpdateSignerStates(newSignerState! as CliqueSignerState) From 014e22d9a2fd630feac2965dfccf220a8e1bd18e Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 14:18:40 -0800 Subject: [PATCH 25/50] add helper: cliqueSaveGenesisSigners --- packages/blockchain/src/index.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index a6d81a1d87..9ef6ead89a 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -297,15 +297,10 @@ export default class Blockchain implements BlockchainInterface { DBSetBlockOrHeader(genesisBlock).map((op) => dbOps.push(op)) DBSaveLookups(genesisHash, new BN(0)).map((op) => dbOps.push(op)) - // Clique: save initial genesis block signers to DB if (this._common.consensusAlgorithm() === 'clique') { - const genesisSignerState = [ - genesisBlock.header.number.toBuffer(), - genesisBlock.header.cliqueEpochTransitionSigners(), - ] - await this.cliqueUpdateSignerStates(genesisSignerState as CliqueSignerState) - await this.cliqueUpdateVotes() + this.cliqueSaveGenesisSigners(genesisBlock) } + await this.dbManager.batch(dbOps) } @@ -389,6 +384,15 @@ export default class Blockchain implements BlockchainInterface { } } + private async cliqueSaveGenesisSigners(genesisBlock: Block) { + const genesisSignerState: CliqueSignerState = [ + genesisBlock.header.number.toBuffer(), + genesisBlock.header.cliqueEpochTransitionSigners(), + ] + await this.cliqueUpdateSignerStates(genesisSignerState) + await this.cliqueUpdateVotes() + } + private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) { const dbOps: DBOp[] = [] if (signerState) { From 7c8f72282c1ebfedd6dbc2021a3fa6dc4dc8a15a Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 14:20:35 -0800 Subject: [PATCH 26/50] rename `_checkClique` to `_requireClique` to be more self descriptive --- packages/block/src/header.ts | 14 +++++++------- packages/blockchain/src/index.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index a004b0f993..da465f68cd 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -529,7 +529,7 @@ export class BlockHeader { return this.number.isZero() } - private _checkClique(name: string) { + private _requireClique(name: string) { if (this._common.consensusAlgorithm() !== 'clique') { throw new Error(`BlockHeader.${name}() call only supported for clique PoA networks`) } @@ -540,7 +540,7 @@ export class BlockHeader { * header (only clique PoA, throws otherwise) */ cliqueIsEpochTransition(): boolean { - this._checkClique('cliqueIsEpochTransition') + 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 @@ -552,7 +552,7 @@ export class BlockHeader { * (only clique PoA, throws otherwise) */ cliqueExtraVanity(): Buffer { - this._checkClique('cliqueExtraVanity') + this._requireClique('cliqueExtraVanity') return this.extraData.slice(0, CLIQUE_EXTRA_VANITY) } @@ -561,7 +561,7 @@ export class BlockHeader { * (only clique PoA, throws otherwise) */ cliqueExtraSeal(): Buffer { - this._checkClique('cliqueExtraSeal') + this._requireClique('cliqueExtraSeal') return this.extraData.slice(-CLIQUE_EXTRA_SEAL) } @@ -574,7 +574,7 @@ export class BlockHeader { * in conjunction with `cliqueIsEpochTransition()` */ cliqueEpochTransitionSigners(): Buffer[] { - this._checkClique('cliqueEpochTransitionSigners') + this._requireClique('cliqueEpochTransitionSigners') if (!this.cliqueIsEpochTransition()) { throw new Error('Singers are only included in epoch transition blocks (clique)') } @@ -598,7 +598,7 @@ export class BlockHeader { * Method throws if signature is invalid */ cliqueVerifySignature(signerList: Buffer[]): boolean { - this._checkClique('cliqueVerifySignature') + this._requireClique('cliqueVerifySignature') const signerAddress = this.cliqueSignatureToAddress() const signerFound = signerList.find((signer) => { return signer.equals(signerAddress.toBuffer()) @@ -613,7 +613,7 @@ export class BlockHeader { * Returns the signer address */ cliqueSignatureToAddress(): Address { - this._checkClique('cliqueSignatureToAddress') + this._requireClique('cliqueSignatureToAddress') const extraSeal = this.cliqueExtraSeal() const r = extraSeal.slice(0, 32) const s = extraSeal.slice(32, 64) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 9ef6ead89a..2eac7b8a50 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -378,7 +378,7 @@ export default class Blockchain implements BlockchainInterface { } } - private _checkClique() { + private _requireClique() { if (this._common.consensusAlgorithm() !== 'clique') { throw new Error('Function call only supported for clique PoA networks') } @@ -464,7 +464,7 @@ export default class Blockchain implements BlockchainInterface { * (only clique PoA, throws otherwise) */ public cliqueActiveSigners() { - this._checkClique() + this._requireClique() return this._cliqueLatestSignerStates[this._cliqueLatestSignerStates.length - 1][1] } From 0641bb8990248fd44c8fa43cd8998f59c186f057 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 14:22:04 -0800 Subject: [PATCH 27/50] move DBTarget enums for clique to bottom of list to not disrupt prior ordering --- packages/blockchain/src/db/operation.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/blockchain/src/db/operation.ts b/packages/blockchain/src/db/operation.ts index e518bea964..c827454b1c 100644 --- a/packages/blockchain/src/db/operation.ts +++ b/packages/blockchain/src/db/operation.ts @@ -21,10 +21,10 @@ export enum DBTarget { HashToNumber, NumberToHash, TotalDifficulty, - CliqueSignerStates, - CliqueVotes, Body, Header, + CliqueSignerStates, + CliqueVotes, } /** @@ -76,14 +76,6 @@ export class DBOp { this.baseDBOp.key = HEAD_BLOCK_KEY break } - case DBTarget.CliqueSignerStates: { - this.baseDBOp.key = CLIQUE_SIGNER_STATES_KEY - break - } - case DBTarget.CliqueVotes: { - this.baseDBOp.key = CLIQUE_VOTES_KEY - break - } case DBTarget.HashToNumber: { this.baseDBOp.key = hashToNumberKey(key!.blockHash!) this.cacheString = 'hashToNumber' @@ -109,6 +101,14 @@ export class DBOp { this.cacheString = 'header' break } + case DBTarget.CliqueSignerStates: { + this.baseDBOp.key = CLIQUE_SIGNER_STATES_KEY + break + } + case DBTarget.CliqueVotes: { + this.baseDBOp.key = CLIQUE_VOTES_KEY + break + } } } From 726102ba599c996fc5957a5274b6fe30e83031e7 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 19:43:45 -0800 Subject: [PATCH 28/50] blockheader: add helper method `cliqueHash()` --- packages/block/src/header.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index da465f68cd..0645d57553 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -36,8 +36,7 @@ export class BlockHeader { public readonly gasLimit: BN public readonly gasUsed: BN public readonly timestamp: BN - // no readonly for extraData to ease signature addition for PoA blocks - public extraData: Buffer + public readonly extraData: Buffer public readonly mixHash: Buffer public readonly nonce: Buffer @@ -514,12 +513,10 @@ export class BlockHeader { * Returns the hash of the block header. */ hash(): Buffer { - const raw = this.raw() - // Hash for PoA clique blocks is created without the seal if (this._common.consensusAlgorithm() === 'clique' && !this.isGenesis()) { - raw[12] = this.extraData.slice(0, this.extraData.length - CLIQUE_EXTRA_SEAL) + return this.cliqueHash() } - return rlphash(raw) + return rlphash(this.raw()) } /** @@ -535,6 +532,13 @@ export class BlockHeader { } } + /* Hash for PoA clique blocks is created without the seal */ + 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) From f57febd67bfe0b3c9dd307b2167a15592b3683c5 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 19 Jan 2021 21:52:04 -0800 Subject: [PATCH 29/50] validate extraData, re-enable timestamp checks --- packages/block/test/header.spec.ts | 127 ++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 10 deletions(-) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 8531150833..5fd197299d 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -3,8 +3,8 @@ import { Address, BN, zeros, KECCAK256_RLP, KECCAK256_RLP_ARRAY } from 'ethereum import Common from '@ethereumjs/common' import { BlockHeader } from '../src/header' import { Block } from '../src' -//import { Mockchain } from './mockchain' -//const testData = require('./testdata/testdata.json') +import { Mockchain } from './mockchain' +const testData = require('./testdata/testdata.json') tape('[Block]: Header functions', function (t) { t.test('should create with default constructor', function (st) { @@ -90,36 +90,143 @@ tape('[Block]: Header functions', function (t) { header = BlockHeader.fromHeaderData({}, { common }) st.ok(header.hash().toString('hex'), 'default block should initialize') + + st.end() + }) + + t.test('should validate extraData', async function (st) { + // PoW + let blockchain = new Mockchain() + let common = new Common({ chain: 'mainnet', hardfork: 'chainstart' }) + let genesis = Block.genesis({}, { common }) + await blockchain.putBlock(genesis) + + const number = 1 + let parentHash = genesis.hash() + const timestamp = Date.now() + let { gasLimit } = genesis.header + let data = { number, parentHash, timestamp, gasLimit } + let opts = { common, calcDifficultyFromHeader: genesis.header } + + // valid extraData: at limit + let testCase = 'pow block should validate with 32 bytes of extraData' + let extraData = Buffer.alloc(32) + let header = BlockHeader.fromHeaderData({ ...data, extraData }, opts) + try { + await header.validate(blockchain) + st.pass(testCase) + } catch (error) { + console.log(error) + st.fail(testCase) + } + + // valid extraData: fewer than limit + testCase = 'pow block should validate with 12 bytes of extraData' + extraData = Buffer.alloc(12) + header = BlockHeader.fromHeaderData({ ...data, extraData }, opts) + try { + await header.validate(blockchain) + st.ok(testCase) + } catch (error) { + st.fail(testCase) + } + + // extraData beyond limit + testCase = 'pow block should throw with excess amount of extraData' + extraData = Buffer.alloc(42) + header = BlockHeader.fromHeaderData({ ...data, extraData }, opts) + try { + await header.validate(blockchain) + st.fail(testCase) + } catch (error) { + st.equal(error.message, 'invalid amount of extra data', testCase) + } + + // PoA + blockchain = new Mockchain() + common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) + genesis = Block.genesis({}, { common }) + await blockchain.putBlock(genesis) + + parentHash = genesis.hash() + gasLimit = genesis.header.gasLimit + data = { number, parentHash, timestamp, gasLimit } + opts = { common } as any + + // valid extraData (32 byte vanity + 65 byte seal) + testCase = 'clique block should validate with valid number of bytes in extraData: 32 byte vanity + 65 byte seal' + extraData = Buffer.concat([Buffer.alloc(32), Buffer.alloc(65)]) + header = BlockHeader.fromHeaderData({ ...data, extraData }, opts) + try { + await header.validate(blockchain) + t.pass(testCase) + } catch (error) { + t.fail(testCase) + } + + // invalid extraData length + testCase = 'clique block should throw on invalid extraData length' + extraData = Buffer.alloc(32) + header = BlockHeader.fromHeaderData({ ...data, extraData }, opts) + try { + await header.validate(blockchain) + t.fail(testCase) + } catch (error) { + t.equal(error.message, 'extraData must be 97 bytes on non-epoch transition blocks, received 32 bytes', testCase) + } + + // signer list indivisible by 20 + testCase = 'clique blocks should throw on invalid extraData length: indivisible by 20' + extraData = Buffer.concat([Buffer.alloc(32), Buffer.alloc(65), Buffer.alloc(20), Buffer.alloc(21)]) + const epoch = new BN(common.consensusConfig().epoch) + header = BlockHeader.fromHeaderData({ ...data, number: epoch, extraData }, opts) + try { + await header.validate(blockchain) + st.fail(testCase) + } catch (error) { + st.equals(error.message, 'invalid signer list length in extraData, received signer length of 41 (not divisible by 20)', testCase) + } + st.end() }) - // TODO: reactivate test using dedicated Rinkeby testdata for PoA tests + // TODO: add test using dedicated Rinkeby testdata for PoA tests // (extract from client output once Goerli or Rinkeby connection possible with Block.toJSON()) - /*t.test('header validation -> poa checks', async function (st) { + t.test('header validation -> poa checks', async function (st) { const headerData = testData.blocks[0].blockHeader + const common = new Common({ chain: 'goerli' }) const blockchain = new Mockchain() - await blockchain.putBlock(Block.fromRLPSerializedBlock(testData.genesisRLP, { common })) - const msg = 'invalid timestamp diff (lower than period)' + const block = Block.fromRLPSerializedBlock(testData.genesisRLP, { common }) + await blockchain.putBlock(block) + + headerData.number = 1 headerData.timestamp = new BN(1422494850) headerData.extraData = Buffer.alloc(97) + headerData.mixHash = Buffer.alloc(32) + + let testCase = 'should throw on lower than period timestamp diffs' let header = BlockHeader.fromHeaderData(headerData, { common }) try { await header.validate(blockchain) + st.fail(testCase) } catch (error) { - st.equal(error.message, msg, 'should throw on lower than period timestamp diffs') + st.equals(error.message, 'invalid timestamp diff (lower than period)', testCase) } + + testCase = 'should not throw on timestamp diff equal to period' headerData.timestamp = new BN(1422494864) header = BlockHeader.fromHeaderData(headerData, { common }) try { await header.validate(blockchain) - st.pass('should not throw on timestamp diff equal to period ') + st.pass(testCase) } catch (error) { - st.notOk('thrown but should not throw') + st.fail(testCase) } + st.end() - })*/ + }) t.test('should test validateGasLimit', function (st) { const testData = require('./testdata/bcBlockGasLimitTest.json').tests From a6f8ea74e7315a2bd404a42f6a0cfb75d30c6b69 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 20 Jan 2021 11:03:30 -0800 Subject: [PATCH 30/50] rename cliqueSignatureToAddress() -> cliqueSigner(): Address --- packages/block/src/header.ts | 6 +++--- packages/block/test/clique.spec.ts | 2 +- packages/blockchain/src/index.ts | 2 +- packages/vm/lib/evm/eei.ts | 9 ++++----- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 0645d57553..223fd2f7ed 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -603,7 +603,7 @@ export class BlockHeader { */ cliqueVerifySignature(signerList: Buffer[]): boolean { this._requireClique('cliqueVerifySignature') - const signerAddress = this.cliqueSignatureToAddress() + const signerAddress = this.cliqueSigner() const signerFound = signerList.find((signer) => { return signer.equals(signerAddress.toBuffer()) }) @@ -616,8 +616,8 @@ export class BlockHeader { /** * Returns the signer address */ - cliqueSignatureToAddress(): Address { - this._requireClique('cliqueSignatureToAddress') + cliqueSigner(): Address { + this._requireClique('cliqueSigner') const extraSeal = this.cliqueExtraSeal() const r = extraSeal.slice(0, 32) const s = extraSeal.slice(32, 64) diff --git a/packages/block/test/clique.spec.ts b/packages/block/test/clique.spec.ts index 66aaa98c74..a189bc28dc 100644 --- a/packages/block/test/clique.spec.ts +++ b/packages/block/test/clique.spec.ts @@ -89,7 +89,7 @@ tape('[Header]: Clique PoA Functionality', function (t) { st.equal(header.extraData.length, 97) st.deepEqual(header.cliqueVerifySignature([A.address]), true, 'should verify signature') st.deepEqual( - header.cliqueSignatureToAddress().toBuffer(), + header.cliqueSigner().toBuffer(), A.address, 'should recover the correct signer address' ) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 2eac7b8a50..2d39f8fe00 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -408,7 +408,7 @@ export default class Blockchain implements BlockchainInterface { // 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3,... const SIGNER_LIMIT = Math.floor(this.cliqueActiveSigners().length / 2) + 1 - const signer = header.cliqueSignatureToAddress().toBuffer() + const signer = header.cliqueSigner().toBuffer() const beneficiary = header.coinbase.toBuffer() const nonce = header.nonce const latestVote = [header.number.toBuffer(), [signer, beneficiary, nonce]] diff --git a/packages/vm/lib/evm/eei.ts b/packages/vm/lib/evm/eei.ts index bd0a6a35ee..6d30312f30 100644 --- a/packages/vm/lib/evm/eei.ts +++ b/packages/vm/lib/evm/eei.ts @@ -259,14 +259,13 @@ export default class EEI { * Returns the block's beneficiary address. */ getBlockCoinbase(): BN { - let coinbase: BN + let coinbase: Address if (this._common.consensusAlgorithm() === 'clique') { - coinbase = new BN(this._env.block.header.cliqueSignatureToAddress().toBuffer()) + coinbase = this._env.block.header.cliqueSigner() } else { - coinbase = new BN(this._env.block.header.coinbase.buf) + coinbase = this._env.block.header.coinbase } - - return coinbase + return new BN(coinbase.toBuffer()) } /** From 33d5894e34614574861689423365e6d38fb88a5e Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 20 Jan 2021 12:25:50 -0800 Subject: [PATCH 31/50] use Address in clique types --- packages/block/src/block.ts | 4 +- packages/block/src/header.ts | 15 +++---- packages/block/test/block.spec.ts | 4 +- packages/block/test/clique.spec.ts | 24 +++++----- packages/blockchain/package.json | 2 +- packages/blockchain/src/clique.ts | 9 ++++ packages/blockchain/src/db/manager.ts | 22 ++++++--- packages/blockchain/src/index.ts | 45 ++++++++----------- .../test/{clique.ts => clique.spec.ts} | 28 +++++++----- .../test/{index.ts => index.spec.ts} | 4 +- .../test/{reorg.ts => reorg.spec.ts} | 0 11 files changed, 87 insertions(+), 70 deletions(-) create mode 100644 packages/blockchain/src/clique.ts rename packages/blockchain/test/{clique.ts => clique.spec.ts} (93%) rename packages/blockchain/test/{index.ts => index.spec.ts} (99%) rename packages/blockchain/test/{reorg.ts => reorg.spec.ts} (100%) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index a584111367..489724f83f 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -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' @@ -172,7 +172,7 @@ export class Block { * transition block and should therefore be used * in conjunction with `cliqueIsEpochTransition()` */ - cliqueEpochTransitionSigners(): Buffer[] { + cliqueEpochTransitionSigners(): Address[] { return this.header.cliqueEpochTransitionSigners() } diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 223fd2f7ed..021781ba12 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -577,7 +577,7 @@ export class BlockHeader { * transition block and should therefore be used * in conjunction with `cliqueIsEpochTransition()` */ - cliqueEpochTransitionSigners(): Buffer[] { + cliqueEpochTransitionSigners(): Address[] { this._requireClique('cliqueEpochTransitionSigners') if (!this.cliqueIsEpochTransition()) { throw new Error('Singers are only included in epoch transition blocks (clique)') @@ -592,7 +592,7 @@ export class BlockHeader { for (let start = 0; start <= signerBuffer.length - signerLength; start += signerLength) { signerList.push(signerBuffer.slice(start, start + signerLength)) } - return signerList + return signerList.map(buf => new Address(buf)) } /** @@ -601,16 +601,13 @@ export class BlockHeader { * * Method throws if signature is invalid */ - cliqueVerifySignature(signerList: Buffer[]): boolean { + cliqueVerifySignature(signerList: Address[]): boolean { this._requireClique('cliqueVerifySignature') - const signerAddress = this.cliqueSigner() + const signerAddress = this.cliqueSigner().toBuffer() const signerFound = signerList.find((signer) => { - return signer.equals(signerAddress.toBuffer()) + return signer.toBuffer().equals(signerAddress) }) - if (signerFound) { - return true - } - return false + return !!signerFound } /** diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index b4274dd398..1f211b1cea 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -1,5 +1,5 @@ import tape from 'tape' -import { BN, rlp, zeros } from 'ethereumjs-util' +import { Address, BN, rlp, zeros } from 'ethereumjs-util' import Common from '@ethereumjs/common' import { Block, BlockBuffer } from '../src' import { Mockchain } from './mockchain' @@ -432,7 +432,7 @@ tape('[Block]: block functions', function (t) { 'header.cliqueExtraSeal() -> should get the header function results' ) const msg = 'header.cliqueEpochTransitionSigners() -> should get the header function results' - st.deepEqual(block.cliqueEpochTransitionSigners(), [Buffer.alloc(20), Buffer.alloc(20)], msg) + st.deepEqual(block.cliqueEpochTransitionSigners(), [Address.zero(), Address.zero()], msg) st.end() }) diff --git a/packages/block/test/clique.spec.ts b/packages/block/test/clique.spec.ts index a189bc28dc..db82f2b2d3 100644 --- a/packages/block/test/clique.spec.ts +++ b/packages/block/test/clique.spec.ts @@ -1,7 +1,7 @@ import tape from 'tape' import Common from '@ethereumjs/common' import { BlockHeader } from '../src/header' -import { ecsign, intToBuffer } from 'ethereumjs-util' +import { Address, ecsign, intToBuffer } from 'ethereumjs-util' tape('[Header]: Clique PoA Functionality', function (t) { const common = new Common({ chain: 'rinkeby', hardfork: 'chainstart' }) @@ -54,19 +54,19 @@ tape('[Header]: Clique PoA Functionality', function (t) { ) const msg = 'cliqueEpochTransitionSigners() -> should return the correct epoch transition signer list on epoch block' - st.deepEqual(header.cliqueEpochTransitionSigners(), [Buffer.alloc(20), Buffer.alloc(20)], msg) + st.deepEqual(header.cliqueEpochTransitionSigners(), [Address.zero(), Address.zero()], msg) st.end() }) type Signer = { - address: Buffer + address: Address privateKey: Buffer publicKey: Buffer } const A: Signer = { - address: Buffer.from('0b90087d864e82a284dca15923f3776de6bb016f', 'hex'), + address: new Address(Buffer.from('0b90087d864e82a284dca15923f3776de6bb016f', 'hex')), privateKey: Buffer.from( '64bf9cc30328b0e42387b3c82c614e6386259136235e20c1357bd11cdee86993', 'hex' @@ -78,19 +78,23 @@ tape('[Header]: Clique PoA Functionality', function (t) { } t.test('Signing', function (st) { - const header = BlockHeader.fromHeaderData( + let header = BlockHeader.fromHeaderData( { number: 1, extraData: Buffer.alloc(97) }, { common, freeze: false } ) const signature = ecsign(header.hash(), A.privateKey) const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) - header.extraData = Buffer.concat([header.cliqueExtraVanity(), signatureB]) + const extraData = Buffer.concat([header.cliqueExtraVanity(), signatureB]) + + header = BlockHeader.fromHeaderData( + { number: 1, extraData }, + { common, freeze: false } + ) st.equal(header.extraData.length, 97) - st.deepEqual(header.cliqueVerifySignature([A.address]), true, 'should verify signature') - st.deepEqual( - header.cliqueSigner().toBuffer(), - A.address, + st.ok(header.cliqueVerifySignature([A.address]), 'should verify signature') + st.ok( + header.cliqueSigner().toBuffer().equals(A.address.toBuffer()), 'should recover the correct signer address' ) diff --git a/packages/blockchain/package.json b/packages/blockchain/package.json index 1653fbbe2d..491931cd7b 100644 --- a/packages/blockchain/package.json +++ b/packages/blockchain/package.json @@ -19,7 +19,7 @@ "tsc": "ethereumjs-config-tsc", "lint": "ethereumjs-config-lint", "lint:fix": "ethereumjs-config-lint-fix", - "test": "tape -r ts-node/register ./test/*.ts" + "test": "tape -r ts-node/register ./test/*.spec.ts" }, "repository": { "type": "git", diff --git a/packages/blockchain/src/clique.ts b/packages/blockchain/src/clique.ts new file mode 100644 index 0000000000..f8c9b9cd1e --- /dev/null +++ b/packages/blockchain/src/clique.ts @@ -0,0 +1,9 @@ +import { Address, BN, rlp } from 'ethereumjs-util' + +export type CliqueSignerState = [BN, Address[]] +export type CliqueLatestSignerStates = CliqueSignerState[] +export type CliqueVote = [BN, [Address, Address, Buffer]] +export type CliqueLatestVotes = CliqueVote[] + +export const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') +export const CLIQUE_NONCE_DROP = Buffer.alloc(8) \ No newline at end of file diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index 53619cb8e9..5cac9033a7 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -1,5 +1,5 @@ import * as rlp from 'rlp' -import { BN } from 'ethereumjs-util' +import { Address, BN } from 'ethereumjs-util' import { Block, BlockHeader, @@ -12,7 +12,7 @@ import Cache from './cache' import { DatabaseKey, DBOp, DBTarget, DBOpData } from './operation' import type { LevelUp } from 'levelup' -import { CliqueLatestSignerStates, CliqueLatestVotes } from '..' +import { CliqueLatestSignerStates, CliqueLatestVotes } from '../clique' const level = require('level-mem') @@ -80,7 +80,12 @@ export class DBManager { async getCliqueLatestSignerStates(): Promise { try { const signerStates = await this.get(DBTarget.CliqueSignerStates) - return rlp.decode(signerStates) as CliqueLatestSignerStates + const states = rlp.decode(signerStates) as [Buffer, Buffer[]] + return states.map(state => { + const blockNum = new BN(state[0]) + const addrs = (state[1]).map((buf: Buffer) => new Address(buf)) + return [blockNum, addrs] + }) as CliqueLatestSignerStates } catch (error) { if (error.type === 'NotFoundError') { return [] @@ -94,8 +99,15 @@ export class DBManager { */ async getCliqueLatestVotes(): Promise { try { - const votes = await this.get(DBTarget.CliqueVotes) - return rlp.decode(votes) as CliqueLatestVotes + const signerVotes = await this.get(DBTarget.CliqueVotes) + const votes = rlp.decode(signerVotes) as [Buffer, [Buffer, Buffer, Buffer]] + return votes.map(vote => { + const blockNum = new BN(vote[0]) + const signer = new Address((vote[1] as any)[0]) + const beneficiary = new Address((vote[1] as any)[1]) + const nonce = (vote[1] as any)[2] + return [blockNum, [signer, beneficiary, nonce]] + }) as CliqueLatestVotes } catch (error) { if (error.type === 'NotFoundError') { return [] diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 2d39f8fe00..6c521b250d 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -1,23 +1,16 @@ import Semaphore from 'semaphore-async-await' -import { BN, rlp } from 'ethereumjs-util' +import { Address, BN, rlp } from 'ethereumjs-util' import { Block, BlockHeader } from '@ethereumjs/block' import Ethash from '@ethereumjs/ethash' import Common from '@ethereumjs/common' import { DBManager } from './db/manager' import { DBOp, DBSetBlockOrHeader, DBSetTD, DBSetHashToNumber, DBSaveLookups } from './db/helpers' import { DBTarget } from './db/operation' +import { CliqueSignerState, CliqueLatestSignerStates, CliqueVote, CliqueLatestVotes, CLIQUE_NONCE_AUTH, CLIQUE_NONCE_DROP } from './clique' import type { LevelUp } from 'levelup' const level = require('level-mem') -export type CliqueSignerState = [Buffer, Buffer[]] -export type CliqueLatestSignerStates = CliqueSignerState[] -export type CliqueVote = [Buffer, [Buffer, Buffer, Buffer]] -export type CliqueLatestVotes = CliqueVote[] - -const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') -const CLIQUE_NONCE_DROP = Buffer.alloc(8) - type OnBlock = (block: Block, reorg: boolean) => Promise | void export interface BlockchainInterface { @@ -386,7 +379,7 @@ export default class Blockchain implements BlockchainInterface { private async cliqueSaveGenesisSigners(genesisBlock: Block) { const genesisSignerState: CliqueSignerState = [ - genesisBlock.header.number.toBuffer(), + genesisBlock.header.number, genesisBlock.header.cliqueEpochTransitionSigners(), ] await this.cliqueUpdateSignerStates(genesisSignerState) @@ -397,7 +390,8 @@ export default class Blockchain implements BlockchainInterface { const dbOps: DBOp[] = [] if (signerState) { this._cliqueLatestSignerStates.push(signerState) - dbOps.push(DBOp.set(DBTarget.CliqueSignerStates, rlp.encode(this._cliqueLatestSignerStates))) + const formatted = this._cliqueLatestSignerStates.map(state => [state[0].toBuffer(), state[1].map(a => a.toBuffer())]) + dbOps.push(DBOp.set(DBTarget.CliqueSignerStates, rlp.encode(formatted))) } await this.dbManager.batch(dbOps) } @@ -408,14 +402,14 @@ export default class Blockchain implements BlockchainInterface { // 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3,... const SIGNER_LIMIT = Math.floor(this.cliqueActiveSigners().length / 2) + 1 - const signer = header.cliqueSigner().toBuffer() - const beneficiary = header.coinbase.toBuffer() + const signer = header.cliqueSigner() + const beneficiary = header.coinbase const nonce = header.nonce - const latestVote = [header.number.toBuffer(), [signer, beneficiary, nonce]] + const latestVote = [header.number, [signer, beneficiary, nonce]] const alreadyVoted = this._cliqueLatestVotes.find((vote: CliqueVote) => { return ( - vote[1][0].equals(signer) && vote[1][1].equals(beneficiary) && vote[1][2].equals(nonce) + vote[1][0].toBuffer().equals(signer.toBuffer()) && vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && vote[1][2].equals(nonce) ) }) ? true @@ -428,7 +422,7 @@ export default class Blockchain implements BlockchainInterface { // to update the signer list if (!alreadyVoted) { const beneficiaryVotes = this._cliqueLatestVotes.filter((vote: CliqueVote) => { - return vote[1][1].equals(beneficiary) && vote[1][2].equals(nonce) + return vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && vote[1][2].equals(nonce) }) const benficiaryVoteNum = beneficiaryVotes.length // Majority consensus @@ -439,23 +433,24 @@ export default class Blockchain implements BlockchainInterface { activeSigners.push(beneficiary) // Drop existing signer } else if (nonce.equals(CLIQUE_NONCE_DROP)) { - activeSigners = activeSigners.filter((signer: Buffer) => { - return !signer.equals(beneficiary) + activeSigners = activeSigners.filter((signer: Address) => { + return !signer.toBuffer().equals(beneficiary.toBuffer()) }) // Discard votes from removed signer this._cliqueLatestVotes = this._cliqueLatestVotes.filter((vote: CliqueVote) => { - return !vote[1][0].equals(beneficiary) + return !vote[1][0].toBuffer().equals(beneficiary.toBuffer()) }) } else { throw new Error('Invalid nonce for clique block: ' + nonce) } - const newSignerState: CliqueSignerState = [header.number.toBuffer(), activeSigners] - await this.cliqueUpdateSignerStates(newSignerState! as CliqueSignerState) + const newSignerState: CliqueSignerState = [header.number, activeSigners] + await this.cliqueUpdateSignerStates(newSignerState) } } } const dbOps: DBOp[] = [] - dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(this._cliqueLatestVotes))) + const formatted = this._cliqueLatestVotes.map(v => [v[0].toBuffer(), [v[1][0].toBuffer(), v[1][0].toBuffer(), v[1][2]]]) + dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(formatted))) await this.dbManager.batch(dbOps) } @@ -463,7 +458,7 @@ export default class Blockchain implements BlockchainInterface { * Returns a list with the current block signers * (only clique PoA, throws otherwise) */ - public cliqueActiveSigners() { + public cliqueActiveSigners(): Address[] { this._requireClique() return this._cliqueLatestSignerStates[this._cliqueLatestSignerStates.length - 1][1] } @@ -628,20 +623,16 @@ export default class Blockchain implements BlockchainInterface { if (this._common.consensusAlgorithm() === 'ethash') { // set total difficulty in the current context scope - console.log('gogo1') if (this._headHeaderHash) { currentTd.header = await this.getTotalDifficulty(this._headHeaderHash) } - console.log('gogo2') if (this._headBlockHash) { currentTd.block = await this.getTotalDifficulty(this._headBlockHash) } - console.log('gogo3') // calculate the total difficulty of the new block const parentTd = await this.getTotalDifficulty(header.parentHash, blockNumber.subn(1)) td.iadd(parentTd) - console.log('gogo4') } // save total difficulty to the database diff --git a/packages/blockchain/test/clique.ts b/packages/blockchain/test/clique.spec.ts similarity index 93% rename from packages/blockchain/test/clique.ts rename to packages/blockchain/test/clique.spec.ts index cc3b9e7acf..b998c73217 100644 --- a/packages/blockchain/test/clique.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -1,6 +1,6 @@ import { Block } from '@ethereumjs/block' import Common from '@ethereumjs/common' -import { intToBuffer, ecsign } from 'ethereumjs-util' +import { Address, intToBuffer, ecsign } from 'ethereumjs-util' import tape from 'tape' import Blockchain from '../src' @@ -25,13 +25,13 @@ tape('Clique: Initialization', (t) => { const GAS_LIMIT = 8000000 type Signer = { - address: Buffer + address: Address privateKey: Buffer publicKey: Buffer } const A: Signer = { - address: Buffer.from('0b90087d864e82a284dca15923f3776de6bb016f', 'hex'), + address: new Address(Buffer.from('0b90087d864e82a284dca15923f3776de6bb016f', 'hex')), privateKey: Buffer.from( '64bf9cc30328b0e42387b3c82c614e6386259136235e20c1357bd11cdee86993', 'hex' @@ -43,7 +43,7 @@ tape('Clique: Initialization', (t) => { } const B: Signer = { - address: Buffer.from('dc7bc81ddf67d037d7439f8e6ff12f3d2a100f71', 'hex'), + address: new Address(Buffer.from('dc7bc81ddf67d037d7439f8e6ff12f3d2a100f71', 'hex')), privateKey: Buffer.from( '86b0ff7b6cf70786f29f297c57562905ab0b6c32d69e177a46491e56da9e486e', 'hex' @@ -55,7 +55,7 @@ tape('Clique: Initialization', (t) => { } const C: Signer = { - address: Buffer.from('8458f408106c4875c96679f3f556a511beabe138', 'hex'), + address: new Address(Buffer.from('8458f408106c4875c96679f3f556a511beabe138', 'hex')), privateKey: Buffer.from( '159e95d07a6c64ddbafa6036cdb7b8114e6e8cdc449ca4b0468a6d0c955f991b', 'hex' @@ -67,7 +67,7 @@ tape('Clique: Initialization', (t) => { } const D: Signer = { - address: Buffer.from('83c30730d1972baa09765a1ac72a43db27fedce5', 'hex'), + address: new Address(Buffer.from('83c30730d1972baa09765a1ac72a43db27fedce5', 'hex')), privateKey: Buffer.from( 'f216ddcf276079043c52b5dd144aa073e6b272ad4bfeaf4fbbc044aa478d1927', 'hex' @@ -79,7 +79,7 @@ tape('Clique: Initialization', (t) => { } const E: Signer = { - address: Buffer.from('6f62d8382bf2587361db73ceca28be91b2acb6df', 'hex'), + address: new Address(Buffer.from('6f62d8382bf2587361db73ceca28be91b2acb6df', 'hex')), privateKey: Buffer.from( '2a6e9ad5a6a8e4f17149b8bc7128bf090566a11dbd63c30e5a0ee9f161309cd6', 'hex' @@ -93,10 +93,10 @@ tape('Clique: Initialization', (t) => { const NONCE_AUTH = Buffer.from('ffffffffffffffff', 'hex') const NONCE_DROP = Buffer.from('0000000000000000', 'hex') - const initWithSigners = (signers: Buffer[]) => { + const initWithSigners = (signers: Address[]) => { const blocks: Block[] = [] - const extraData = Buffer.concat([Buffer.alloc(32), ...signers, Buffer.alloc(65)]) + const extraData = Buffer.concat([Buffer.alloc(32), ...signers.map(s => s.toBuffer()), Buffer.alloc(65)]) const genesisBlock = Block.genesis( { header: { gasLimit: GAS_LIMIT, extraData } }, { common: COMMON } @@ -121,7 +121,7 @@ tape('Clique: Initialization', (t) => { const number = blocks.length const lastBlock = blocks[number - 1] - let coinbase = Buffer.alloc(20) + let coinbase = Address.zero() let nonce = NONCE_DROP if (beneficiary) { coinbase = beneficiary[0].address @@ -141,10 +141,14 @@ tape('Clique: Initialization', (t) => { nonce, }, } - const block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) + let block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) const signature = ecsign(block.header.hash(), signer.privateKey) const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) - block.header.extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) + + const extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) + blockData.header.extraData = extraData + + block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) await blockchain.putBlock(block) blocks.push(block) diff --git a/packages/blockchain/test/index.ts b/packages/blockchain/test/index.spec.ts similarity index 99% rename from packages/blockchain/test/index.ts rename to packages/blockchain/test/index.spec.ts index 58883e51dc..057ee3ba5f 100644 --- a/packages/blockchain/test/index.ts +++ b/packages/blockchain/test/index.spec.ts @@ -38,10 +38,10 @@ tape('blockchain test', (t) => { }) common = new Common({ chain: 'goerli' }) - st.throws(() => { + st.doesNotThrow(() => { new Blockchain({ common, validateConsensus: true }) }) - st.throws(() => { + st.doesNotThrow(() => { new Blockchain({ common, validateBlocks: true }) }) st.end() diff --git a/packages/blockchain/test/reorg.ts b/packages/blockchain/test/reorg.spec.ts similarity index 100% rename from packages/blockchain/test/reorg.ts rename to packages/blockchain/test/reorg.spec.ts From 4aea0d57e250d97c4be90bd76a2bafb40bd81225 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 20 Jan 2021 12:34:01 -0800 Subject: [PATCH 32/50] lint:fix --- packages/block/src/header.ts | 2 +- packages/block/test/clique.spec.ts | 5 +---- packages/block/test/header.spec.ts | 24 ++++++++++++++++------ packages/blockchain/src/clique.ts | 4 ++-- packages/blockchain/src/db/manager.ts | 8 ++++---- packages/blockchain/src/index.ts | 27 +++++++++++++++++++------ packages/blockchain/test/clique.spec.ts | 6 +++++- 7 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 021781ba12..dcaa2c66bb 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -592,7 +592,7 @@ export class BlockHeader { for (let start = 0; start <= signerBuffer.length - signerLength; start += signerLength) { signerList.push(signerBuffer.slice(start, start + signerLength)) } - return signerList.map(buf => new Address(buf)) + return signerList.map((buf) => new Address(buf)) } /** diff --git a/packages/block/test/clique.spec.ts b/packages/block/test/clique.spec.ts index db82f2b2d3..f42f2574c9 100644 --- a/packages/block/test/clique.spec.ts +++ b/packages/block/test/clique.spec.ts @@ -86,10 +86,7 @@ tape('[Header]: Clique PoA Functionality', function (t) { const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) const extraData = Buffer.concat([header.cliqueExtraVanity(), signatureB]) - header = BlockHeader.fromHeaderData( - { number: 1, extraData }, - { common, freeze: false } - ) + header = BlockHeader.fromHeaderData({ number: 1, extraData }, { common, freeze: false }) st.equal(header.extraData.length, 97) st.ok(header.cliqueVerifySignature([A.address]), 'should verify signature') diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 5fd197299d..9ba65dd5ae 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -90,7 +90,6 @@ tape('[Block]: Header functions', function (t) { header = BlockHeader.fromHeaderData({}, { common }) st.ok(header.hash().toString('hex'), 'default block should initialize') - st.end() }) @@ -116,7 +115,6 @@ tape('[Block]: Header functions', function (t) { await header.validate(blockchain) st.pass(testCase) } catch (error) { - console.log(error) st.fail(testCase) } @@ -154,7 +152,8 @@ tape('[Block]: Header functions', function (t) { opts = { common } as any // valid extraData (32 byte vanity + 65 byte seal) - testCase = 'clique block should validate with valid number of bytes in extraData: 32 byte vanity + 65 byte seal' + testCase = + 'clique block should validate with valid number of bytes in extraData: 32 byte vanity + 65 byte seal' extraData = Buffer.concat([Buffer.alloc(32), Buffer.alloc(65)]) header = BlockHeader.fromHeaderData({ ...data, extraData }, opts) try { @@ -172,19 +171,32 @@ tape('[Block]: Header functions', function (t) { await header.validate(blockchain) t.fail(testCase) } catch (error) { - t.equal(error.message, 'extraData must be 97 bytes on non-epoch transition blocks, received 32 bytes', testCase) + t.equal( + error.message, + 'extraData must be 97 bytes on non-epoch transition blocks, received 32 bytes', + testCase + ) } // signer list indivisible by 20 testCase = 'clique blocks should throw on invalid extraData length: indivisible by 20' - extraData = Buffer.concat([Buffer.alloc(32), Buffer.alloc(65), Buffer.alloc(20), Buffer.alloc(21)]) + extraData = Buffer.concat([ + Buffer.alloc(32), + Buffer.alloc(65), + Buffer.alloc(20), + Buffer.alloc(21), + ]) const epoch = new BN(common.consensusConfig().epoch) header = BlockHeader.fromHeaderData({ ...data, number: epoch, extraData }, opts) try { await header.validate(blockchain) st.fail(testCase) } catch (error) { - st.equals(error.message, 'invalid signer list length in extraData, received signer length of 41 (not divisible by 20)', testCase) + st.equals( + error.message, + 'invalid signer list length in extraData, received signer length of 41 (not divisible by 20)', + testCase + ) } st.end() diff --git a/packages/blockchain/src/clique.ts b/packages/blockchain/src/clique.ts index f8c9b9cd1e..95f2f1590b 100644 --- a/packages/blockchain/src/clique.ts +++ b/packages/blockchain/src/clique.ts @@ -1,4 +1,4 @@ -import { Address, BN, rlp } from 'ethereumjs-util' +import { Address, BN } from 'ethereumjs-util' export type CliqueSignerState = [BN, Address[]] export type CliqueLatestSignerStates = CliqueSignerState[] @@ -6,4 +6,4 @@ export type CliqueVote = [BN, [Address, Address, Buffer]] export type CliqueLatestVotes = CliqueVote[] export const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') -export const CLIQUE_NONCE_DROP = Buffer.alloc(8) \ No newline at end of file +export const CLIQUE_NONCE_DROP = Buffer.alloc(8) diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index 5cac9033a7..57dd0cd9f5 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -80,8 +80,8 @@ export class DBManager { async getCliqueLatestSignerStates(): Promise { try { const signerStates = await this.get(DBTarget.CliqueSignerStates) - const states = rlp.decode(signerStates) as [Buffer, Buffer[]] - return states.map(state => { + const states = (rlp.decode(signerStates)) as [Buffer, Buffer[]] + return states.map((state) => { const blockNum = new BN(state[0]) const addrs = (state[1]).map((buf: Buffer) => new Address(buf)) return [blockNum, addrs] @@ -100,8 +100,8 @@ export class DBManager { async getCliqueLatestVotes(): Promise { try { const signerVotes = await this.get(DBTarget.CliqueVotes) - const votes = rlp.decode(signerVotes) as [Buffer, [Buffer, Buffer, Buffer]] - return votes.map(vote => { + const votes = (rlp.decode(signerVotes)) as [Buffer, [Buffer, Buffer, Buffer]] + return votes.map((vote) => { const blockNum = new BN(vote[0]) const signer = new Address((vote[1] as any)[0]) const beneficiary = new Address((vote[1] as any)[1]) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 6c521b250d..52d7babf79 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -6,7 +6,14 @@ import Common from '@ethereumjs/common' import { DBManager } from './db/manager' import { DBOp, DBSetBlockOrHeader, DBSetTD, DBSetHashToNumber, DBSaveLookups } from './db/helpers' import { DBTarget } from './db/operation' -import { CliqueSignerState, CliqueLatestSignerStates, CliqueVote, CliqueLatestVotes, CLIQUE_NONCE_AUTH, CLIQUE_NONCE_DROP } from './clique' +import { + CliqueSignerState, + CliqueLatestSignerStates, + CliqueVote, + CliqueLatestVotes, + CLIQUE_NONCE_AUTH, + CLIQUE_NONCE_DROP, +} from './clique' import type { LevelUp } from 'levelup' const level = require('level-mem') @@ -291,7 +298,7 @@ export default class Blockchain implements BlockchainInterface { DBSaveLookups(genesisHash, new BN(0)).map((op) => dbOps.push(op)) if (this._common.consensusAlgorithm() === 'clique') { - this.cliqueSaveGenesisSigners(genesisBlock) + await this.cliqueSaveGenesisSigners(genesisBlock) } await this.dbManager.batch(dbOps) @@ -390,7 +397,10 @@ export default class Blockchain implements BlockchainInterface { const dbOps: DBOp[] = [] if (signerState) { this._cliqueLatestSignerStates.push(signerState) - const formatted = this._cliqueLatestSignerStates.map(state => [state[0].toBuffer(), state[1].map(a => a.toBuffer())]) + const formatted = this._cliqueLatestSignerStates.map((state) => [ + state[0].toBuffer(), + state[1].map((a) => a.toBuffer()), + ]) dbOps.push(DBOp.set(DBTarget.CliqueSignerStates, rlp.encode(formatted))) } await this.dbManager.batch(dbOps) @@ -409,7 +419,9 @@ export default class Blockchain implements BlockchainInterface { const alreadyVoted = this._cliqueLatestVotes.find((vote: CliqueVote) => { return ( - vote[1][0].toBuffer().equals(signer.toBuffer()) && vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && vote[1][2].equals(nonce) + vote[1][0].toBuffer().equals(signer.toBuffer()) && + vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && + vote[1][2].equals(nonce) ) }) ? true @@ -441,7 +453,7 @@ export default class Blockchain implements BlockchainInterface { return !vote[1][0].toBuffer().equals(beneficiary.toBuffer()) }) } else { - throw new Error('Invalid nonce for clique block: ' + nonce) + throw new Error(`Invalid nonce for clique block: ${nonce.toString('hex')}`) } const newSignerState: CliqueSignerState = [header.number, activeSigners] await this.cliqueUpdateSignerStates(newSignerState) @@ -449,7 +461,10 @@ 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]]]) + const formatted = this._cliqueLatestVotes.map((v) => [ + v[0].toBuffer(), + [v[1][0].toBuffer(), v[1][0].toBuffer(), v[1][2]], + ]) dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(formatted))) await this.dbManager.batch(dbOps) } diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index b998c73217..5de089cfec 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -96,7 +96,11 @@ tape('Clique: Initialization', (t) => { const initWithSigners = (signers: Address[]) => { const blocks: Block[] = [] - const extraData = Buffer.concat([Buffer.alloc(32), ...signers.map(s => s.toBuffer()), Buffer.alloc(65)]) + const extraData = Buffer.concat([ + Buffer.alloc(32), + ...signers.map((s) => s.toBuffer()), + Buffer.alloc(65), + ]) const genesisBlock = Block.genesis( { header: { gasLimit: GAS_LIMIT, extraData } }, { common: COMMON } From 0411971d1deda0039b867c4bfe642874668f0f65 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 10:44:52 -0800 Subject: [PATCH 33/50] move DBSetTd inside if(ethash) --- packages/blockchain/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 52d7babf79..c303df400c 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -648,10 +648,10 @@ export default class Blockchain implements BlockchainInterface { // calculate the total difficulty of the new block const parentTd = await this.getTotalDifficulty(header.parentHash, blockNumber.subn(1)) td.iadd(parentTd) - } - // save total difficulty to the database - dbOps = dbOps.concat(DBSetTD(td, blockNumber, blockHash)) + // save total difficulty to the database + dbOps = dbOps.concat(DBSetTD(td, blockNumber, blockHash)) + } // save header/block to the database dbOps = dbOps.concat(DBSetBlockOrHeader(block)) From 74618be9b05a62cf18a699e98f1ae88eef31f7c6 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 11:00:14 -0800 Subject: [PATCH 34/50] use non-clique chain for mocked test --- packages/vm/tests/api/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/tests/api/index.spec.ts b/packages/vm/tests/api/index.spec.ts index 0ef56941c7..7c42976942 100644 --- a/packages/vm/tests/api/index.spec.ts +++ b/packages/vm/tests/api/index.spec.ts @@ -120,7 +120,7 @@ tape('VM with blockchain', (t) => { }) t.test('should run blockchain with mocked runBlock', async (st) => { - const common = new Common({ chain: 'goerli' }) + const common = new Common({ chain: 'ropsten' }) const genesisRlp = Buffer.from(testData.genesisRLP.slice(2), 'hex') const genesisBlock = Block.fromRLPSerializedBlock(genesisRlp, { common }) From 00dc55af63df9948b10130f51ac448e9bdb5b95c Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 12:33:43 -0800 Subject: [PATCH 35/50] use exported clique nonce constants --- packages/blockchain/test/clique.spec.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index 5de089cfec..0accf83ee0 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -3,6 +3,7 @@ import Common from '@ethereumjs/common' import { Address, intToBuffer, ecsign } from 'ethereumjs-util' import tape from 'tape' import Blockchain from '../src' +import { CLIQUE_NONCE_AUTH, CLIQUE_NONCE_DROP } from '../src/clique' tape('Clique: Initialization', (t) => { t.test('should initialize a clique blockchain', async (st) => { @@ -90,9 +91,6 @@ tape('Clique: Initialization', (t) => { ), } - const NONCE_AUTH = Buffer.from('ffffffffffffffff', 'hex') - const NONCE_DROP = Buffer.from('0000000000000000', 'hex') - const initWithSigners = (signers: Address[]) => { const blocks: Block[] = [] @@ -126,11 +124,11 @@ tape('Clique: Initialization', (t) => { const lastBlock = blocks[number - 1] let coinbase = Address.zero() - let nonce = NONCE_DROP + let nonce = CLIQUE_NONCE_DROP if (beneficiary) { coinbase = beneficiary[0].address if (beneficiary[1]) { - nonce = NONCE_AUTH + nonce = CLIQUE_NONCE_AUTH } } From 6d723ce9e926513bfb5cc61b4de53f5ab9296eb1 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 12:46:43 -0800 Subject: [PATCH 36/50] add block poa header tests: * non-zero beneficiary (coinbase) on epoch transition blocks * non-zero mixHash --- packages/block/test/header.spec.ts | 34 ++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 9ba65dd5ae..b0fb546f25 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -202,8 +202,6 @@ tape('[Block]: Header functions', function (t) { st.end() }) - // TODO: add test using dedicated Rinkeby testdata for PoA tests - // (extract from client output once Goerli or Rinkeby connection possible with Block.toJSON()) t.test('header validation -> poa checks', async function (st) { const headerData = testData.blocks[0].blockHeader @@ -237,6 +235,38 @@ tape('[Block]: Header functions', function (t) { st.fail(testCase) } + testCase = 'should throw on non-zero beneficiary (coinbase) for epoch transition block' + headerData.number = common.consensusConfig().epoch + headerData.coinbase = Address.fromString('0x091dcd914fCEB1d47423e532955d1E62d1b2dAEf') + header = BlockHeader.fromHeaderData(headerData, { common }) + try { + await header.validate(blockchain) + st.fail('should throw') + } catch (error) { + if (error.message.includes('coinbase must be filled with zeros on epoch transition blocks')) { + st.pass('error thrown') + } else { + st.fail('should throw with appropriate error') + } + } + headerData.number = 1 + headerData.coinbase = Address.zero() + + testCase = 'should throw on non-zero mixHash' + headerData.mixHash = Buffer.alloc(32).fill(1) + header = BlockHeader.fromHeaderData(headerData, { common }) + try { + await header.validate(blockchain) + st.fail('should throw') + } catch (error) { + if (error.message.includes('mixHash must be filled with zeros')) { + st.pass('error thrown') + } else { + st.fail('should throw with appropriate error') + } + } + headerData.mixHash = Buffer.alloc(32) + st.end() }) From edab558aea3627bc92d870dacf4f131ad4b61e9c Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 13:58:42 -0800 Subject: [PATCH 37/50] valiidate checkpoint signers toward active signers --- packages/blockchain/src/index.ts | 13 +++++++++---- packages/blockchain/test/clique.spec.ts | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index c303df400c..5c5aafae51 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -678,10 +678,15 @@ export default class Blockchain implements BlockchainInterface { if (header.cliqueIsEpochTransition()) { this._cliqueLatestVotes = [] await this.cliqueUpdateVotes() - // TODO: on epoch transition blocks we might want to validate the - // calculated active signer list towards the epoch block - // signer checkpoint list in the extraData field - // Add eventual new header vote on non-epoch transition blocks + + // validate checkpoint signers towards active signers + const checkpointSigners = header.cliqueEpochTransitionSigners() + const activeSigners = this.cliqueActiveSigners() + for (const cSigner of checkpointSigners) { + if (!activeSigners.find(aSigner => aSigner.toBuffer().equals(cSigner.toBuffer()))) { + throw new Error('checkpoint signer not found in active signers list: ' + cSigner.toString()) + } + } } else { await this.cliqueUpdateVotes(header) } diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index 0accf83ee0..fcbc4a46bb 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -157,6 +157,26 @@ tape('Clique: Initialization', (t) => { return block } + t.test('should throw if signer in epoch checkpoint is not active', async (st) => { + const { blockchain } = initWithSigners([A.address]) + ; (blockchain as any)._validateBlocks = false + const number = COMMON.consensusConfig().epoch + const unauthorizedSigner = Address.fromString('0x00a839de7922491683f547a67795204763ff8237') + const extraData = Buffer.concat([Buffer.alloc(32), A.address.toBuffer(), unauthorizedSigner.toBuffer(), Buffer.alloc(65)]) + const block = Block.fromBlockData({ header: { number, extraData, } }, { common: COMMON }) + try { + await blockchain.putBlock(block) + st.fail('should fail') + } catch (error) { + if (error.message.includes('checkpoint signer not found in active signers list: ' + unauthorizedSigner.toString())) { + st.pass('correct error') + } else { + st.fail('should fail with appropriate error') + } + } + st.end() + }) + // Test Cases: https://eips.ethereum.org/EIPS/eip-225 t.test('Clique Voting: Single signer, no votes cast', async (st) => { const { blocks, blockchain } = initWithSigners([A.address]) From fee1381105f1ecc751f843c29fbc210da4214de6 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 14:00:36 -0800 Subject: [PATCH 38/50] trim clique signer states and latest votes to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT --- packages/blockchain/src/index.ts | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 5c5aafae51..45c64cc870 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -393,16 +393,23 @@ export default class Blockchain implements BlockchainInterface { await this.cliqueUpdateVotes() } - private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) { + private async cliqueUpdateSignerStates(signerState: CliqueSignerState) { const dbOps: DBOp[] = [] - if (signerState) { - this._cliqueLatestSignerStates.push(signerState) - const formatted = this._cliqueLatestSignerStates.map((state) => [ - state[0].toBuffer(), - state[1].map((a) => a.toBuffer()), - ]) - dbOps.push(DBOp.set(DBTarget.CliqueSignerStates, rlp.encode(formatted))) + 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) } + + const formatted = this._cliqueLatestSignerStates.map((state) => [ + state[0].toBuffer(), + state[1].map((a) => a.toBuffer()), + ]) + dbOps.push(DBOp.set(DBTarget.CliqueSignerStates, rlp.encode(formatted))) + await this.dbManager.batch(dbOps) } @@ -460,12 +467,21 @@ export default class Blockchain implements BlockchainInterface { } } } + + // trim length to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT + const length = this._cliqueLatestVotes.length + const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT + if (length > limit) { + this._cliqueLatestVotes = this._cliqueLatestVotes.slice(length - limit, length) + } + const dbOps: DBOp[] = [] const formatted = this._cliqueLatestVotes.map((v) => [ v[0].toBuffer(), [v[1][0].toBuffer(), v[1][0].toBuffer(), v[1][2]], ]) dbOps.push(DBOp.set(DBTarget.CliqueVotes, rlp.encode(formatted))) + await this.dbManager.batch(dbOps) } From def4177ff4db54790f9693177c9881c4f1f5d501 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 14:45:38 -0800 Subject: [PATCH 39/50] validate goerli poa block (additional fix: pass options correctly to blockFromRpc) --- packages/block/src/from-rpc.ts | 2 +- packages/block/test/block.spec.ts | 33 +++++++---- .../testdata/testdata-from-rpc-goerli.json | 55 +++++++++++++++++++ 3 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 packages/block/test/testdata/testdata-from-rpc-goerli.json diff --git a/packages/block/src/from-rpc.ts b/packages/block/src/from-rpc.ts index f0cb183f69..60bd049537 100644 --- a/packages/block/src/from-rpc.ts +++ b/packages/block/src/from-rpc.ts @@ -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) } diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index 1f211b1cea..7d52a826d9 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -2,8 +2,10 @@ import tape from 'tape' import { Address, BN, rlp, zeros } from 'ethereumjs-util' import Common from '@ethereumjs/common' import { Block, BlockBuffer } from '../src' +import blockFromRpc from '../src/from-rpc' import { Mockchain } from './mockchain' import { createBlock } from './util' +import * as testDataFromRpcGoerli from './testdata/testdata-from-rpc-goerli.json' tape('[Block]: block functions', function (t) { t.test('should test block initialization', function (st) { @@ -97,21 +99,28 @@ tape('[Block]: block functions', function (t) { }) }) - // TODO: reactivate test using dedicated Rinkeby testdata for PoA tests - // (extract from client output once Goerli or Rinkeby connection possible with Block.toJSON()) - /*t.test('should test block validation on poa chain', async function (st) { + t.test('should test block validation on poa chain', async function (st) { const common = new Common({ chain: 'goerli', hardfork: 'chainstart' }) - const blockRlp = testData.blocks[0].rlp - const block = Block.fromRLPSerializedBlock(blockRlp, { common }) const blockchain = new Mockchain() - const genesisBlock = Block.fromRLPSerializedBlock(testData.genesisRLP, { common }) - console.log(genesisBlock.header.hash()) - await blockchain.putBlock(genesisBlock) - st.doesNotThrow(async () => { + const block = blockFromRpc(testDataFromRpcGoerli, [], { common }) + + const genesis = Block.genesis({}, { common }) + await blockchain.putBlock(genesis) + + const parentBlock = Block.fromBlockData({ header: { number: block.header.number.subn(1), timestamp: block.header.timestamp.subn(1000), gasLimit: block.header.gasLimit } }, { common, freeze: false }) + parentBlock.hash = () => { return block.header.parentHash } + await blockchain.putBlock(parentBlock) + + await blockchain.putBlock(block) + try { await block.validate(blockchain) - st.end() - }) - })*/ + st.pass('does not throw') + } catch (error) { + console.log(error) + st.fail('error thrown') + } + st.end() + }) async function testTransactionValidation(st: tape.Test, block: Block) { st.ok(block.validateTransactions()) diff --git a/packages/block/test/testdata/testdata-from-rpc-goerli.json b/packages/block/test/testdata/testdata-from-rpc-goerli.json new file mode 100644 index 0000000000..6a3e39e957 --- /dev/null +++ b/packages/block/test/testdata/testdata-from-rpc-goerli.json @@ -0,0 +1,55 @@ +{ + "difficulty": "0x1", + "extraData": "0x696e667572612d696f0000000000000000000000000000000000000000000000c321f5bc793e6eb41a4a319e8bd605aaa9316a24ad9b7d3af7b924176500fcf71424f2833fdc64326fb1b2229bc70943fed8d8446e4803f6c65503f0a1535a0c00", + "gasLimit": "0x7a1200", + "gasUsed": "0x2dcb6", + "hash": "0xc54c5b482baefc20932c8be06db0a7b22ce26283438f51761e5c3e16e5376054", + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000040000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000008000000000000000000000000000000000000000001000000080008000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000500000000000000000000000000000002000040000000000000000000000000000000000000000000000008000000000000000000000100000000000000000000000000020000000000008000000000000000000000000", + "miner": "0x0000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0xf4240", + "parentHash": "0x9b6323bc7b5f651ad6c7943d776209323834b10d76a53e109a4a8982138d0e38", + "receiptsRoot": "0xda46cdd329bfedace32da95f2b344d314bc6f55f027d65f9f4ac04ee425e1f98", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x402", + "stateRoot": "0x0535b699f40b9cb637fb89e0662281d63f10207d04f2371fd9c4cb9b7b15e9d6", + "timestamp": "0x5d390640", + "totalDifficulty": "0x175dac", + "transactions": [ + { + "blockHash": "0xc54c5b482baefc20932c8be06db0a7b22ce26283438f51761e5c3e16e5376054", + "blockNumber": "0xf4240", + "from": "0x79047abf3af2a1061b108d71d6dc7bdb06474790", + "gas": "0x3d090", + "gasPrice": "0x3b9aca00", + "hash": "0xc2c3ba07f05ddd8552508e7facf25dc5bd6d16e95c12cff42cb8b9ea6bbfc225", + "input": "0xe9c6c176000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000bd9fe6f7af1cc94b1aef2e0fa15f1b4baefa86eb60e78fa4bd082372a0a446d197fb58", + "nonce": "0x2687f", + "r": "0xd0eeac4841caf7a894dd79e6e633efc2380553cdf8b786d1aa0b8a8dee0266f4", + "s": "0x740710eed9696c663510b7fb71a553112551121595a54ec6d2ec0afcec72a973", + "to": "0x7ef66b77759e12caf3ddb3e4aff524e577c59d8d", + "transactionIndex": "0x0", + "v": "0x1c", + "value": "0x0" + }, + { + "blockHash": "0xc54c5b482baefc20932c8be06db0a7b22ce26283438f51761e5c3e16e5376054", + "blockNumber": "0xf4240", + "from": "0x79047abf3af2a1061b108d71d6dc7bdb06474790", + "gas": "0x3d090", + "gasPrice": "0x3b9aca00", + "hash": "0xca8a182f21b98318e94ec7884f572c0a1385dbc10a2bea62a38079eab7d8cfef", + "input": "0xe9c6c176000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000004920eaa814f7df6a2203dc0e472e8828be95957c6b329fee8e2b1bb6f044c1eb4fc243", + "nonce": "0x26880", + "r": "0xa3ff39967683fc684dc7b857d6f62723e78804a14b091a058ad95cc1b8a0281f", + "s": "0x51b156e05f21f499fa1ae47ebf536b15a237208f1d4a62e33956b6b03cf47742", + "to": "0x7ef66b77759e12caf3ddb3e4aff524e577c59d8d", + "transactionIndex": "0x1", + "v": "0x1b", + "value": "0x0" + } + ], + "transactionsRoot": "0x3ab7120d12e1fc07303508542602beb7eecfe8f262b83fd71eefe7d6205242ce", + "uncles": [] +} From ba4bb83d92a53e5280acc109fa7429473fe570e8 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Jan 2021 18:17:11 -0800 Subject: [PATCH 40/50] lint:fix --- packages/block/test/block.spec.ts | 16 +++++++++++++--- packages/blockchain/src/index.ts | 6 ++++-- packages/blockchain/test/clique.spec.ts | 17 +++++++++++++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/block/test/block.spec.ts b/packages/block/test/block.spec.ts index 7d52a826d9..22736b09eb 100644 --- a/packages/block/test/block.spec.ts +++ b/packages/block/test/block.spec.ts @@ -107,8 +107,19 @@ tape('[Block]: block functions', function (t) { const genesis = Block.genesis({}, { common }) await blockchain.putBlock(genesis) - const parentBlock = Block.fromBlockData({ header: { number: block.header.number.subn(1), timestamp: block.header.timestamp.subn(1000), gasLimit: block.header.gasLimit } }, { common, freeze: false }) - parentBlock.hash = () => { return block.header.parentHash } + const parentBlock = Block.fromBlockData( + { + header: { + number: block.header.number.subn(1), + timestamp: block.header.timestamp.subn(1000), + gasLimit: block.header.gasLimit, + }, + }, + { common, freeze: false } + ) + parentBlock.hash = () => { + return block.header.parentHash + } await blockchain.putBlock(parentBlock) await blockchain.putBlock(block) @@ -116,7 +127,6 @@ tape('[Block]: block functions', function (t) { await block.validate(blockchain) st.pass('does not throw') } catch (error) { - console.log(error) st.fail('error thrown') } st.end() diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 45c64cc870..58d3f2b848 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -699,8 +699,10 @@ export default class Blockchain implements BlockchainInterface { const checkpointSigners = header.cliqueEpochTransitionSigners() const activeSigners = this.cliqueActiveSigners() for (const cSigner of checkpointSigners) { - if (!activeSigners.find(aSigner => aSigner.toBuffer().equals(cSigner.toBuffer()))) { - throw new Error('checkpoint signer not found in active signers list: ' + cSigner.toString()) + if (!activeSigners.find((aSigner) => aSigner.toBuffer().equals(cSigner.toBuffer()))) { + throw new Error( + 'checkpoint signer not found in active signers list: ' + cSigner.toString() + ) } } } else { diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index fcbc4a46bb..cedd900795 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -159,16 +159,25 @@ tape('Clique: Initialization', (t) => { t.test('should throw if signer in epoch checkpoint is not active', async (st) => { const { blockchain } = initWithSigners([A.address]) - ; (blockchain as any)._validateBlocks = false + ;(blockchain as any)._validateBlocks = false const number = COMMON.consensusConfig().epoch const unauthorizedSigner = Address.fromString('0x00a839de7922491683f547a67795204763ff8237') - const extraData = Buffer.concat([Buffer.alloc(32), A.address.toBuffer(), unauthorizedSigner.toBuffer(), Buffer.alloc(65)]) - const block = Block.fromBlockData({ header: { number, extraData, } }, { common: COMMON }) + const extraData = Buffer.concat([ + Buffer.alloc(32), + A.address.toBuffer(), + unauthorizedSigner.toBuffer(), + Buffer.alloc(65), + ]) + const block = Block.fromBlockData({ header: { number, extraData } }, { common: COMMON }) try { await blockchain.putBlock(block) st.fail('should fail') } catch (error) { - if (error.message.includes('checkpoint signer not found in active signers list: ' + unauthorizedSigner.toString())) { + if ( + error.message.includes( + 'checkpoint signer not found in active signers list: ' + unauthorizedSigner.toString() + ) + ) { st.pass('correct error') } else { st.fail('should fail with appropriate error') From 48da84e6495c6adf6a5faca851d0dc3b87d4dbf1 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Jan 2021 17:39:46 -0800 Subject: [PATCH 41/50] add comments with clique type annotations --- packages/blockchain/src/clique.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/blockchain/src/clique.ts b/packages/blockchain/src/clique.ts index 95f2f1590b..eebcfb3479 100644 --- a/packages/blockchain/src/clique.ts +++ b/packages/blockchain/src/clique.ts @@ -1,9 +1,10 @@ import { Address, BN } from 'ethereumjs-util' -export type CliqueSignerState = [BN, Address[]] +export type CliqueSignerState = [BN, Address[]] // [blockNumber, signers] export type CliqueLatestSignerStates = CliqueSignerState[] -export type CliqueVote = [BN, [Address, Address, Buffer]] + +export type CliqueVote = [BN, [Address, Address, Buffer]] // [blockNumber, [signer, beneficiary, cliqueNonce]] export type CliqueLatestVotes = CliqueVote[] -export const CLIQUE_NONCE_AUTH: Buffer = Buffer.from('ffffffffffffffff', 'hex') +export const CLIQUE_NONCE_AUTH = Buffer.from('ffffffffffffffff', 'hex') export const CLIQUE_NONCE_DROP = Buffer.alloc(8) From d69444f03e5d485c0d98682bc5a6048b12413e12 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Jan 2021 19:42:42 -0800 Subject: [PATCH 42/50] add validation for clique difficulty --- packages/block/src/header.ts | 38 +++++++- packages/block/src/types.ts | 3 +- packages/block/test/header.spec.ts | 3 +- packages/block/test/mockchain.ts | 3 + packages/blockchain/src/index.ts | 17 ++-- packages/blockchain/test/clique.spec.ts | 118 +++++++++++++++++++----- 6 files changed, 146 insertions(+), 36 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index dcaa2c66bb..67fcaeff02 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -20,6 +20,9 @@ const DEFAULT_GAS_LIMIT = new BN(Buffer.from('ffffffffffffff', 'hex')) const CLIQUE_EXTRA_VANITY = 32 const CLIQUE_EXTRA_SEAL = 65 +const CLIQUE_DIFF_INTURN = new BN(2) +const CLIQUE_DIFF_NOTURN = new BN(1) + /** * An object that represents the block header. */ @@ -363,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. @@ -443,6 +470,9 @@ export class BlockHeader { 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 parentHeader = await this._getHeaderByHash(blockchain, this.parentHash) @@ -460,7 +490,7 @@ export class BlockHeader { throw new Error('invalid timestamp') } - if (this._common.consensusAlgorithm() == 'clique') { + 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)) { diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 6c7ded0a98..a1cc0339af 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -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' @@ -130,4 +130,5 @@ export interface JsonHeader { export interface Blockchain { getBlock(hash: Buffer): Promise + cliqueActiveSigners(): Address[] } diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index b0fb546f25..c92bc43ef5 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -148,7 +148,7 @@ tape('[Block]: Header functions', function (t) { parentHash = genesis.hash() gasLimit = genesis.header.gasLimit - data = { number, parentHash, timestamp, gasLimit } + data = { number, parentHash, timestamp, gasLimit, difficulty: new BN(1) } as any opts = { common } as any // valid extraData (32 byte vanity + 65 byte seal) @@ -215,6 +215,7 @@ tape('[Block]: Header functions', function (t) { headerData.timestamp = new BN(1422494850) headerData.extraData = Buffer.alloc(97) headerData.mixHash = Buffer.alloc(32) + headerData.difficulty = new BN(2) let testCase = 'should throw on lower than period timestamp diffs' let header = BlockHeader.fromHeaderData(headerData, { common }) diff --git a/packages/block/test/mockchain.ts b/packages/block/test/mockchain.ts index 4b0c7af252..ebb2a3c4eb 100644 --- a/packages/block/test/mockchain.ts +++ b/packages/block/test/mockchain.ts @@ -10,4 +10,7 @@ export class Mockchain implements Blockchain { async putBlock(block: Block) { this.HashMap[block.hash().toString('hex')] = block } + cliqueActiveSigners() { + return [] + } } diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 58d3f2b848..354251b572 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -422,9 +422,9 @@ export default class Blockchain implements BlockchainInterface { const signer = header.cliqueSigner() const beneficiary = header.coinbase const nonce = header.nonce - const latestVote = [header.number, [signer, beneficiary, nonce]] + const latestVote: CliqueVote = [header.number, [signer, beneficiary, nonce]] - const alreadyVoted = this._cliqueLatestVotes.find((vote: CliqueVote) => { + const alreadyVoted = this._cliqueLatestVotes.find((vote) => { return ( vote[1][0].toBuffer().equals(signer.toBuffer()) && vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && @@ -436,13 +436,14 @@ export default class Blockchain implements BlockchainInterface { // Always add the latest vote to the history no matter if already voted // the same vote or not - this._cliqueLatestVotes.push(latestVote as CliqueVote) + this._cliqueLatestVotes.push(latestVote) + // If same vote not already in history see if there is a new majority consensus // to update the signer list if (!alreadyVoted) { - const beneficiaryVotes = this._cliqueLatestVotes.filter((vote: CliqueVote) => { - return vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && vote[1][2].equals(nonce) - }) + const beneficiaryVotes = this._cliqueLatestVotes.filter( + (vote) => vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && vote[1][2].equals(nonce) + ) const benficiaryVoteNum = beneficiaryVotes.length // Majority consensus if (benficiaryVoteNum >= SIGNER_LIMIT) { @@ -452,11 +453,11 @@ export default class Blockchain implements BlockchainInterface { activeSigners.push(beneficiary) // Drop existing signer } else if (nonce.equals(CLIQUE_NONCE_DROP)) { - activeSigners = activeSigners.filter((signer: Address) => { + activeSigners = activeSigners.filter((signer) => { return !signer.toBuffer().equals(beneficiary.toBuffer()) }) // Discard votes from removed signer - this._cliqueLatestVotes = this._cliqueLatestVotes.filter((vote: CliqueVote) => { + this._cliqueLatestVotes = this._cliqueLatestVotes.filter((vote) => { return !vote[1][0].toBuffer().equals(beneficiary.toBuffer()) }) } else { diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index cedd900795..54a6bebc9f 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -1,6 +1,6 @@ import { Block } from '@ethereumjs/block' import Common from '@ethereumjs/common' -import { Address, intToBuffer, ecsign } from 'ethereumjs-util' +import { Address, intToBuffer, ecsign, BN } from 'ethereumjs-util' import tape from 'tape' import Blockchain from '../src' import { CLIQUE_NONCE_AUTH, CLIQUE_NONCE_DROP } from '../src/clique' @@ -91,7 +91,7 @@ tape('Clique: Initialization', (t) => { ), } - const initWithSigners = (signers: Address[]) => { + const initWithSigners = async (signers: Address[]) => { const blocks: Block[] = [] const extraData = Buffer.concat([ @@ -105,7 +105,7 @@ tape('Clique: Initialization', (t) => { ) blocks.push(genesisBlock) - const blockchain = new Blockchain({ + const blockchain = await Blockchain.create({ validateBlocks: true, validateConsensus: false, genesisBlock, @@ -140,13 +140,24 @@ tape('Clique: Initialization', (t) => { timestamp: lastBlock.header.timestamp.addn(15), extraData: EXTRA_DATA, gasLimit: GAS_LIMIT, + difficulty: new BN(2), nonce, }, } + + // calculate difficulty + const signers = blockchain.cliqueActiveSigners() + const signerIndex = signers.findIndex((address: Address) => + address.toBuffer().equals(signer.address.toBuffer()) + ) + const inTurn = number % signers.length == signerIndex + blockData.header.difficulty = inTurn ? new BN(2) : new BN(1) + let block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) + + // add signature in extraData const signature = ecsign(block.header.hash(), signer.privateKey) const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) - const extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) blockData.header.extraData = extraData @@ -158,7 +169,7 @@ tape('Clique: Initialization', (t) => { } t.test('should throw if signer in epoch checkpoint is not active', async (st) => { - const { blockchain } = initWithSigners([A.address]) + const { blockchain } = await initWithSigners([A.address]) ;(blockchain as any)._validateBlocks = false const number = COMMON.consensusConfig().epoch const unauthorizedSigner = Address.fromString('0x00a839de7922491683f547a67795204763ff8237') @@ -186,9 +197,57 @@ tape('Clique: Initialization', (t) => { st.end() }) + t.test('should throw on invalid difficulty', async (st) => { + const { blocks, blockchain } = await initWithSigners([A.address]) + await addNextBlock(blockchain, blocks, A) + ;(blockchain as any)._validateBlocks = false + + const number = new BN(1) + let extraData = Buffer.alloc(97) + let difficulty = new BN(5) + let block = Block.fromBlockData( + { header: { number, extraData, difficulty } }, + { common: COMMON } + ) + + try { + await block.validate(blockchain) + st.fail('should fail') + } catch (error) { + if (error.message.includes('difficulty for clique block must be INTURN (2) or NOTURN (1)')) { + st.pass('correct error') + } else { + st.fail('should fail with appropriate error') + } + } + + difficulty = new BN(1) + block = Block.fromBlockData({ header: { number, extraData, difficulty } }, { common: COMMON }) + + // add signature + const signature = ecsign(block.header.hash(), A.privateKey) + const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) + extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) + + block = Block.fromBlockData({ header: { number, extraData, difficulty } }, { common: COMMON }) + + try { + await block.validate(blockchain) + st.fail('should fail') + } catch (error) { + if (error.message.includes('invalid clique difficulty')) { + st.pass('correct error') + } else { + st.fail('should fail with appropriate error') + } + } + + st.end() + }) + // Test Cases: https://eips.ethereum.org/EIPS/eip-225 t.test('Clique Voting: Single signer, no votes cast', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A.address]) const block = await addNextBlock(blockchain, blocks, A) st.equal(block.header.number.toNumber(), 1) st.deepEqual(blockchain.cliqueActiveSigners(), [A.address]) @@ -196,7 +255,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Single signer, voting to add two others', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A.address]) await addNextBlock(blockchain, blocks, A, [B, true]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [C, true]) @@ -209,7 +268,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Two signers, voting to add three others', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address]) await addNextBlock(blockchain, blocks, A, [C, true]) await addNextBlock(blockchain, blocks, B, [C, true]) await addNextBlock(blockchain, blocks, A, [D, true]) @@ -227,7 +286,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Single signer, dropping itself)', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A.address]) await addNextBlock(blockchain, blocks, A, [A, false]) st.deepEqual( @@ -241,7 +300,7 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Two signers, actually needing mutual consent to drop either of them', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address]) await addNextBlock(blockchain, blocks, A, [B, false]) st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], 'not fulfilled') @@ -252,7 +311,7 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Two signers, actually needing mutual consent to drop either of them', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address]) await addNextBlock(blockchain, blocks, A, [B, false]) await addNextBlock(blockchain, blocks, B, [B, false]) @@ -262,7 +321,7 @@ tape('Clique: Initialization', (t) => { ) t.test('Clique Voting: Three signers, two of them deciding to drop the third', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -273,7 +332,12 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Four signers, consensus of two not being enough to drop anyone', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + const { blocks, blockchain } = await initWithSigners([ + A.address, + B.address, + C.address, + D.address, + ]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -285,7 +349,12 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Four signers, consensus of three already being enough to drop someone', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + const { blocks, blockchain } = await initWithSigners([ + A.address, + B.address, + C.address, + D.address, + ]) await addNextBlock(blockchain, blocks, A, [D, false]) await addNextBlock(blockchain, blocks, B, [D, false]) await addNextBlock(blockchain, blocks, C, [D, false]) @@ -296,7 +365,7 @@ tape('Clique: Initialization', (t) => { ) t.test('Clique Voting: Authorizations are counted once per signer per target', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address]) await addNextBlock(blockchain, blocks, A, [C, true]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [C, true]) @@ -308,7 +377,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Authorizing multiple accounts concurrently is permitted', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address]) await addNextBlock(blockchain, blocks, A, [C, true]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [D, true]) @@ -323,7 +392,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Deauthorizations are counted once per signer per target', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address]) await addNextBlock(blockchain, blocks, A, [B, false]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [B, false]) @@ -335,7 +404,12 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Deauthorizing multiple accounts concurrently is permitted', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + const { blocks, blockchain } = await initWithSigners([ + A.address, + B.address, + C.address, + D.address, + ]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, C) @@ -353,7 +427,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Votes from deauthorized signers are discarded immediately', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address]) await addNextBlock(blockchain, blocks, C, [B, false]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -364,7 +438,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Votes from deauthorized signers are discarded immediately', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address]) await addNextBlock(blockchain, blocks, C, [D, true]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -376,7 +450,7 @@ tape('Clique: Initialization', (t) => { // TODO: fix test case /*t.test('Clique Voting: Changes reaching consensus out of bounds (via a deauth) execute on touch', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address, B.address, C.address, D.address]) + const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address, D.address]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, C) @@ -396,7 +470,7 @@ tape('Clique: Initialization', (t) => { // TODO: add two additional test cases, further last test cases not relevant yet t.test('Clique Voting: ', async (st) => { - const { blocks, blockchain } = initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A.address]) await addNextBlock(blockchain, blocks, A, [B, true]) st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') From 2ad875fe67cda8e1679bb9c224c60094c6478a52 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Jan 2021 23:25:35 -0800 Subject: [PATCH 43/50] update param for initWithSigners to use Signer[] to dedupe code in test cases --- packages/blockchain/test/clique.spec.ts | 59 +++++++++---------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index 54a6bebc9f..672247a2d5 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -91,12 +91,12 @@ tape('Clique: Initialization', (t) => { ), } - const initWithSigners = async (signers: Address[]) => { + const initWithSigners = async (signers: Signer[]) => { const blocks: Block[] = [] const extraData = Buffer.concat([ Buffer.alloc(32), - ...signers.map((s) => s.toBuffer()), + ...signers.map((s) => s.address.toBuffer()), Buffer.alloc(65), ]) const genesisBlock = Block.genesis( @@ -150,7 +150,7 @@ tape('Clique: Initialization', (t) => { const signerIndex = signers.findIndex((address: Address) => address.toBuffer().equals(signer.address.toBuffer()) ) - const inTurn = number % signers.length == signerIndex + const inTurn = number % signers.length === signerIndex blockData.header.difficulty = inTurn ? new BN(2) : new BN(1) let block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) @@ -169,7 +169,7 @@ tape('Clique: Initialization', (t) => { } t.test('should throw if signer in epoch checkpoint is not active', async (st) => { - const { blockchain } = await initWithSigners([A.address]) + const { blockchain } = await initWithSigners([A]) ;(blockchain as any)._validateBlocks = false const number = COMMON.consensusConfig().epoch const unauthorizedSigner = Address.fromString('0x00a839de7922491683f547a67795204763ff8237') @@ -198,7 +198,7 @@ tape('Clique: Initialization', (t) => { }) t.test('should throw on invalid difficulty', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A]) await addNextBlock(blockchain, blocks, A) ;(blockchain as any)._validateBlocks = false @@ -247,7 +247,7 @@ tape('Clique: Initialization', (t) => { // Test Cases: https://eips.ethereum.org/EIPS/eip-225 t.test('Clique Voting: Single signer, no votes cast', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A]) const block = await addNextBlock(blockchain, blocks, A) st.equal(block.header.number.toNumber(), 1) st.deepEqual(blockchain.cliqueActiveSigners(), [A.address]) @@ -255,7 +255,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Single signer, voting to add two others', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A]) await addNextBlock(blockchain, blocks, A, [B, true]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [C, true]) @@ -268,7 +268,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Two signers, voting to add three others', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address]) + 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]) @@ -286,7 +286,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Single signer, dropping itself)', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A]) await addNextBlock(blockchain, blocks, A, [A, false]) st.deepEqual( @@ -300,7 +300,7 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Two signers, actually needing mutual consent to drop either of them', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A, B]) await addNextBlock(blockchain, blocks, A, [B, false]) st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], 'not fulfilled') @@ -311,7 +311,7 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Two signers, actually needing mutual consent to drop either of them', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A, B]) await addNextBlock(blockchain, blocks, A, [B, false]) await addNextBlock(blockchain, blocks, B, [B, false]) @@ -321,7 +321,7 @@ tape('Clique: Initialization', (t) => { ) t.test('Clique Voting: Three signers, two of them deciding to drop the third', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address]) + const { blocks, blockchain } = await initWithSigners([A, B, C]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -332,12 +332,7 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Four signers, consensus of two not being enough to drop anyone', async (st) => { - const { blocks, blockchain } = await initWithSigners([ - A.address, - B.address, - C.address, - D.address, - ]) + const { blocks, blockchain } = await initWithSigners([A, B, C, D]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -349,12 +344,7 @@ tape('Clique: Initialization', (t) => { t.test( 'Clique Voting: Four signers, consensus of three already being enough to drop someone', async (st) => { - const { blocks, blockchain } = await initWithSigners([ - A.address, - B.address, - C.address, - D.address, - ]) + const { blocks, blockchain } = await initWithSigners([A, B, C, D]) await addNextBlock(blockchain, blocks, A, [D, false]) await addNextBlock(blockchain, blocks, B, [D, false]) await addNextBlock(blockchain, blocks, C, [D, false]) @@ -365,7 +355,7 @@ tape('Clique: Initialization', (t) => { ) t.test('Clique Voting: Authorizations are counted once per signer per target', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A, B]) await addNextBlock(blockchain, blocks, A, [C, true]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [C, true]) @@ -377,7 +367,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Authorizing multiple accounts concurrently is permitted', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A, B]) await addNextBlock(blockchain, blocks, A, [C, true]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [D, true]) @@ -392,7 +382,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Deauthorizations are counted once per signer per target', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address]) + const { blocks, blockchain } = await initWithSigners([A, B]) await addNextBlock(blockchain, blocks, A, [B, false]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, A, [B, false]) @@ -404,12 +394,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Deauthorizing multiple accounts concurrently is permitted', async (st) => { - const { blocks, blockchain } = await initWithSigners([ - A.address, - B.address, - C.address, - D.address, - ]) + const { blocks, blockchain } = await initWithSigners([A, B, C, D]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, C) @@ -427,7 +412,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Votes from deauthorized signers are discarded immediately', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address]) + const { blocks, blockchain } = await initWithSigners([A, B, C]) await addNextBlock(blockchain, blocks, C, [B, false]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -438,7 +423,7 @@ tape('Clique: Initialization', (t) => { }) t.test('Clique Voting: Votes from deauthorized signers are discarded immediately', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address]) + const { blocks, blockchain } = await initWithSigners([A, B, C]) await addNextBlock(blockchain, blocks, C, [D, true]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B, [C, false]) @@ -450,7 +435,7 @@ tape('Clique: Initialization', (t) => { // TODO: fix test case /*t.test('Clique Voting: Changes reaching consensus out of bounds (via a deauth) execute on touch', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address, B.address, C.address, D.address]) + const { blocks, blockchain } = await initWithSigners([A, B, C, D]) await addNextBlock(blockchain, blocks, A, [C, false]) await addNextBlock(blockchain, blocks, B) await addNextBlock(blockchain, blocks, C) @@ -470,7 +455,7 @@ tape('Clique: Initialization', (t) => { // TODO: add two additional test cases, further last test cases not relevant yet t.test('Clique Voting: ', async (st) => { - const { blocks, blockchain } = await initWithSigners([A.address]) + const { blocks, blockchain } = await initWithSigners([A]) await addNextBlock(blockchain, blocks, A, [B, true]) st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') From 9ec8583a4b116d4f783f257b03a23465a40ae58e Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 26 Jan 2021 21:02:01 -0800 Subject: [PATCH 44/50] fix typo --- packages/block/src/header.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 67fcaeff02..b46659ad07 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -610,7 +610,7 @@ export class BlockHeader { cliqueEpochTransitionSigners(): Address[] { this._requireClique('cliqueEpochTransitionSigners') if (!this.cliqueIsEpochTransition()) { - throw new Error('Singers are only included in epoch transition blocks (clique)') + throw new Error('Signers are only included in epoch transition blocks (clique)') } const start = CLIQUE_EXTRA_VANITY From 3f983e4c2f48c834a9fce2b08d87c827e200d2a1 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 26 Jan 2021 21:07:53 -0800 Subject: [PATCH 45/50] improve clique vote calculations to pass test cases in EIP-225: * remove contradictory votes for [signer, beneficiary] * clear all votes for signer after signer is added or removed * when beneficiary vote is touched/counted, count both auth and drop votes --- packages/blockchain/src/index.ts | 62 +++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 354251b572..2bd41eba3a 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -413,6 +413,11 @@ export default class Blockchain implements BlockchainInterface { await this.dbManager.batch(dbOps) } + /** + * Update clique votes and save to db + * @param header BlockHeader + * @hidden + */ private async cliqueUpdateVotes(header?: BlockHeader) { // Block contains a vote on a new signer if (header && !header.coinbase.isZero()) { @@ -438,30 +443,52 @@ export default class Blockchain implements BlockchainInterface { // the same vote or not this._cliqueLatestVotes.push(latestVote) + // remove any opposite votes for [signer, beneficiary] + const oppositeNonce = nonce.equals(CLIQUE_NONCE_AUTH) ? CLIQUE_NONCE_DROP : CLIQUE_NONCE_AUTH + this._cliqueLatestVotes = this._cliqueLatestVotes.filter( + (vote) => + !( + vote[1][0].toBuffer().equals(signer.toBuffer()) && + vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && + vote[1][2].equals(oppositeNonce) + ) + ) + // If same vote not already in history see if there is a new majority consensus // to update the signer list if (!alreadyVoted) { - const beneficiaryVotes = this._cliqueLatestVotes.filter( - (vote) => vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && vote[1][2].equals(nonce) + const beneficiaryVotesAuth = this._cliqueLatestVotes.filter( + (vote) => + vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && + vote[1][2].equals(CLIQUE_NONCE_AUTH) + ) + const beneficiaryVotesDrop = this._cliqueLatestVotes.filter( + (vote) => + vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && + vote[1][2].equals(CLIQUE_NONCE_DROP) ) - const benficiaryVoteNum = beneficiaryVotes.length + const consensus = + beneficiaryVotesAuth.length >= SIGNER_LIMIT || beneficiaryVotesDrop.length >= SIGNER_LIMIT + const auth = beneficiaryVotesAuth.length >= SIGNER_LIMIT // Majority consensus - if (benficiaryVoteNum >= SIGNER_LIMIT) { + if (consensus) { let activeSigners = this.cliqueActiveSigners() - // Authorize new signer - if (nonce.equals(CLIQUE_NONCE_AUTH)) { + if (auth) { + // Authorize new signer activeSigners.push(beneficiary) - // Drop existing signer - } else if (nonce.equals(CLIQUE_NONCE_DROP)) { - activeSigners = activeSigners.filter((signer) => { - return !signer.toBuffer().equals(beneficiary.toBuffer()) - }) - // Discard votes from removed signer - this._cliqueLatestVotes = this._cliqueLatestVotes.filter((vote) => { - return !vote[1][0].toBuffer().equals(beneficiary.toBuffer()) - }) + // Discard votes for added signer + this._cliqueLatestVotes = this._cliqueLatestVotes.filter( + (vote) => !vote[1][1].toBuffer().equals(beneficiary.toBuffer()) + ) } else { - throw new Error(`Invalid nonce for clique block: ${nonce.toString('hex')}`) + // Drop signer + activeSigners = activeSigners.filter( + (signer) => !signer.toBuffer().equals(beneficiary.toBuffer()) + ) + // Discard votes from removed signer + this._cliqueLatestVotes = this._cliqueLatestVotes.filter( + (vote) => !vote[1][0].toBuffer().equals(beneficiary.toBuffer()) + ) } const newSignerState: CliqueSignerState = [header.number, activeSigners] await this.cliqueUpdateSignerStates(newSignerState) @@ -469,13 +496,14 @@ export default class Blockchain implements BlockchainInterface { } } - // trim length to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT + // trim latest votes length to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT const length = this._cliqueLatestVotes.length const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT if (length > limit) { this._cliqueLatestVotes = this._cliqueLatestVotes.slice(length - limit, length) } + // save votes to db const dbOps: DBOp[] = [] const formatted = this._cliqueLatestVotes.map((v) => [ v[0].toBuffer(), From d9e5558d25b9bc389dd80a6dc7d256fcaf5ef2f0 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 26 Jan 2021 21:14:34 -0800 Subject: [PATCH 46/50] * add CliqueLatestBlockSigners for snapshot for calculating error "recently signed" * add remaining EIP-225 test cases --- packages/blockchain/src/clique.ts | 3 + packages/blockchain/src/db/constants.ts | 6 + packages/blockchain/src/db/manager.ts | 22 ++- packages/blockchain/src/db/operation.ts | 6 + packages/blockchain/src/index.ts | 94 +++++++++- packages/blockchain/test/clique.spec.ts | 233 ++++++++++++++++++++---- packages/common/src/types.ts | 4 + 7 files changed, 328 insertions(+), 40 deletions(-) diff --git a/packages/blockchain/src/clique.ts b/packages/blockchain/src/clique.ts index eebcfb3479..c701320e59 100644 --- a/packages/blockchain/src/clique.ts +++ b/packages/blockchain/src/clique.ts @@ -6,5 +6,8 @@ export type CliqueLatestSignerStates = CliqueSignerState[] export type CliqueVote = [BN, [Address, Address, Buffer]] // [blockNumber, [signer, beneficiary, cliqueNonce]] export type CliqueLatestVotes = CliqueVote[] +export type CliqueBlockSigner = [BN, Address] // [blockNumber, signer] +export type CliqueLatestBlockSigners = CliqueBlockSigner[] + export const CLIQUE_NONCE_AUTH = Buffer.from('ffffffffffffffff', 'hex') export const CLIQUE_NONCE_DROP = Buffer.alloc(8) diff --git a/packages/blockchain/src/db/constants.ts b/packages/blockchain/src/db/constants.ts index a12bedd89b..6eb631b1e9 100644 --- a/packages/blockchain/src/db/constants.ts +++ b/packages/blockchain/src/db/constants.ts @@ -24,6 +24,11 @@ const CLIQUE_SIGNERS_KEY = 'CliqueSigners' */ const CLIQUE_VOTES_KEY = 'CliqueVotes' +/** + * Cique block signers (snapshot) + */ +const CLIQUE_BLOCK_SIGNERS_KEY = 'CliqueBlockSignersSnapshot' + /** * headerPrefix + number + hash -> header */ @@ -75,6 +80,7 @@ export { HEAD_BLOCK_KEY, CLIQUE_SIGNERS_KEY, CLIQUE_VOTES_KEY, + CLIQUE_BLOCK_SIGNERS_KEY, bufBE8, tdKey, headerKey, diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index 57dd0cd9f5..22df3fb7c3 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -12,7 +12,7 @@ import Cache from './cache' import { DatabaseKey, DBOp, DBTarget, DBOpData } from './operation' import type { LevelUp } from 'levelup' -import { CliqueLatestSignerStates, CliqueLatestVotes } from '../clique' +import { CliqueLatestSignerStates, CliqueLatestVotes, CliqueLatestBlockSigners } from '../clique' const level = require('level-mem') @@ -116,6 +116,26 @@ export class DBManager { } } + /** + * Fetches snapshot of clique signers. + */ + async getCliqueLatestBlockSigners(): Promise { + try { + const blockSigners = await this.get(DBTarget.CliqueBlockSigners) + const signers = (rlp.decode(blockSigners)) as [Buffer, Buffer][] + return signers.map((s) => { + const blockNum = new BN(s[0]) + const signer = new Address(s[1] as any) + return [blockNum, signer] + }) as CliqueLatestBlockSigners + } catch (error) { + if (error.type === 'NotFoundError') { + return [] + } + throw error + } + } + /** * Fetches a block (header and body) given a block id, * which can be either its hash or its number. diff --git a/packages/blockchain/src/db/operation.ts b/packages/blockchain/src/db/operation.ts index c827454b1c..87e63f060b 100644 --- a/packages/blockchain/src/db/operation.ts +++ b/packages/blockchain/src/db/operation.ts @@ -10,6 +10,7 @@ import { hashToNumberKey, CLIQUE_SIGNERS_KEY as CLIQUE_SIGNER_STATES_KEY, CLIQUE_VOTES_KEY, + CLIQUE_BLOCK_SIGNERS_KEY, } from './constants' import { CacheMap } from './manager' @@ -25,6 +26,7 @@ export enum DBTarget { Header, CliqueSignerStates, CliqueVotes, + CliqueBlockSigners, } /** @@ -109,6 +111,10 @@ export class DBOp { this.baseDBOp.key = CLIQUE_VOTES_KEY break } + case DBTarget.CliqueBlockSigners: { + this.baseDBOp.key = CLIQUE_BLOCK_SIGNERS_KEY + break + } } } diff --git a/packages/blockchain/src/index.ts b/packages/blockchain/src/index.ts index 2bd41eba3a..18c9db3541 100644 --- a/packages/blockchain/src/index.ts +++ b/packages/blockchain/src/index.ts @@ -11,6 +11,8 @@ import { CliqueLatestSignerStates, CliqueVote, CliqueLatestVotes, + CliqueBlockSigner, + CliqueLatestBlockSigners, CLIQUE_NONCE_AUTH, CLIQUE_NONCE_DROP, } from './clique' @@ -166,6 +168,15 @@ export default class Blockchain implements BlockchainInterface { */ private _cliqueLatestVotes: CliqueLatestVotes = [] + /** + * List of signers for the last consecutive `this.cliqueSignerLimit()` blocks. + * 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. + */ + private _cliqueLatestBlockSigners: CliqueLatestBlockSigners = [] + /** * Creates new Blockchain object * @@ -308,6 +319,7 @@ export default class Blockchain implements BlockchainInterface { if (this._common.consensusAlgorithm() === 'clique') { this._cliqueLatestSignerStates = await this.dbManager.getCliqueLatestSignerStates() this._cliqueLatestVotes = await this.dbManager.getCliqueLatestVotes() + this._cliqueLatestBlockSigners = await this.dbManager.getCliqueLatestBlockSigners() } // At this point, we can safely set genesisHash as the _genesis hash in this @@ -384,6 +396,32 @@ export default class Blockchain implements BlockchainInterface { } } + /** + * Checks if signer was recently signed. + * Returns true if signed too recently: more than once per `this.cliqueSignerLimit()` consecutive blocks. + * @param header BlockHeader + * @hidden + */ + private cliqueCheckRecentlySigned(header: BlockHeader): boolean { + if (header.isGenesis() || header.number.eqn(1)) { + // skip genesis, first block + return false + } + const limit = this.cliqueSignerLimit() + // construct recent block signers list with this block + let signers = this._cliqueLatestBlockSigners + signers = signers.slice(signers.length < limit ? 0 : 1) + signers.push([header.number, header.cliqueSigner()]) + const seen = signers.filter((s) => s[1].toBuffer().equals(header.cliqueSigner().toBuffer())) + .length + return seen > 1 + } + + /** + * Save genesis signers to db + * @param genesisBlock genesis block + * @hidden + */ private async cliqueSaveGenesisSigners(genesisBlock: Block) { const genesisSignerState: CliqueSignerState = [ genesisBlock.header.number, @@ -421,9 +459,6 @@ export default class Blockchain implements BlockchainInterface { private async cliqueUpdateVotes(header?: BlockHeader) { // Block contains a vote on a new signer if (header && !header.coinbase.isZero()) { - // 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3,... - const SIGNER_LIMIT = Math.floor(this.cliqueActiveSigners().length / 2) + 1 - const signer = header.cliqueSigner() const beneficiary = header.coinbase const nonce = header.nonce @@ -467,9 +502,10 @@ export default class Blockchain implements BlockchainInterface { vote[1][1].toBuffer().equals(beneficiary.toBuffer()) && vote[1][2].equals(CLIQUE_NONCE_DROP) ) + const limit = this.cliqueSignerLimit() const consensus = - beneficiaryVotesAuth.length >= SIGNER_LIMIT || beneficiaryVotesDrop.length >= SIGNER_LIMIT - const auth = beneficiaryVotesAuth.length >= SIGNER_LIMIT + beneficiaryVotesAuth.length >= limit || beneficiaryVotesDrop.length >= limit + const auth = beneficiaryVotesAuth.length >= limit // Majority consensus if (consensus) { let activeSigners = this.cliqueActiveSigners() @@ -514,6 +550,36 @@ export default class Blockchain implements BlockchainInterface { await this.dbManager.batch(dbOps) } + /** + * Update snapshot of latest clique block signers. + * Length trimmed to `this.cliqueSignerLimit()` + * @param header BlockHeader + * @hidden + */ + private async cliqueUpdateLatestBlockSigners(header: BlockHeader) { + if (header.isGenesis()) { + return + } + const dbOps: DBOp[] = [] + + // 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 + const formatted = this._cliqueLatestBlockSigners.map((b) => [b[0].toBuffer(), b[1].toBuffer()]) + dbOps.push(DBOp.set(DBTarget.CliqueBlockSigners, rlp.encode(formatted))) + + await this.dbManager.batch(dbOps) + } + /** * Returns a list with the current block signers * (only clique PoA, throws otherwise) @@ -523,6 +589,17 @@ export default class Blockchain implements BlockchainInterface { return this._cliqueLatestSignerStates[this._cliqueLatestSignerStates.length - 1][1] } + /** + * Number of consecutive blocks out of which a signer may only sign one. + * Defined as `Math.floor(SIGNER_COUNT / 2) + 1` to enforce majority consensus. + * signer count -> signer limit: + * 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3, ... + * @hidden + */ + private cliqueSignerLimit() { + return Math.floor(this.cliqueActiveSigners().length / 2) + 1 + } + /** * Returns the specified iterator head. * @@ -673,11 +750,16 @@ export default class Blockchain implements BlockchainInterface { throw new Error('invalid POW') } } + if (this._common.consensusAlgorithm() === 'clique') { const valid = header.cliqueVerifySignature(this.cliqueActiveSigners()) if (!valid) { throw new Error('invalid PoA block signature (clique)') } + + if (this.cliqueCheckRecentlySigned(header)) { + throw new Error('recently signed') + } } } @@ -737,6 +819,8 @@ export default class Blockchain implements BlockchainInterface { } else { await this.cliqueUpdateVotes(header) } + + await this.cliqueUpdateLatestBlockSigners(header) } // delete higher number assignments and overwrite stale canonical chain diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index 672247a2d5..108341a74a 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -91,7 +91,20 @@ tape('Clique: Initialization', (t) => { ), } - const initWithSigners = async (signers: Signer[]) => { + const F: Signer = { + address: new Address(Buffer.from('ab80a948c661aa32d09952d2a6c4ad77a4c947be', 'hex')), + privateKey: Buffer.from( + '48ec5a6c4a7fc67b10a9d4c8a8f594a81ae42e41ed061fa5218d96abb6012344', + 'hex' + ), + publicKey: Buffer.from( + 'adefb82b9f54e80aa3532263e4478739de16fcca6828f4ae842f8a07941c347fa59d2da1300569237009f0f122dc1fd6abb0db8fcb534280aa94948a5cc95f94', + 'hex' + ), + } + + const initWithSigners = async (signers: Signer[], common?: Common) => { + common = common ?? COMMON const blocks: Block[] = [] const extraData = Buffer.concat([ @@ -99,17 +112,14 @@ tape('Clique: Initialization', (t) => { ...signers.map((s) => s.address.toBuffer()), Buffer.alloc(65), ]) - const genesisBlock = Block.genesis( - { header: { gasLimit: GAS_LIMIT, extraData } }, - { common: COMMON } - ) + const genesisBlock = Block.genesis({ header: { gasLimit: GAS_LIMIT, extraData } }, { common }) blocks.push(genesisBlock) const blockchain = await Blockchain.create({ validateBlocks: true, - validateConsensus: false, + validateConsensus: true, genesisBlock, - common: COMMON, + common, }) return { blocks, blockchain } } @@ -118,18 +128,28 @@ tape('Clique: Initialization', (t) => { blockchain: Blockchain, blocks: Block[], signer: Signer, - beneficiary?: [Signer, boolean] + 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 = Buffer.concat([ + Buffer.alloc(32), + ...checkpointSigners.map((s) => s.address.toBuffer()), + Buffer.alloc(65), + ]) } const blockData = { @@ -138,7 +158,7 @@ tape('Clique: Initialization', (t) => { parentHash: lastBlock.hash(), coinbase, timestamp: lastBlock.header.timestamp.addn(15), - extraData: EXTRA_DATA, + extraData, gasLimit: GAS_LIMIT, difficulty: new BN(2), nonce, @@ -153,15 +173,15 @@ tape('Clique: Initialization', (t) => { const inTurn = number % signers.length === signerIndex blockData.header.difficulty = inTurn ? new BN(2) : new BN(1) - let block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) + let block = Block.fromBlockData(blockData, { common, freeze: false }) // add signature in extraData const signature = ecsign(block.header.hash(), signer.privateKey) const signatureB = Buffer.concat([signature.r, signature.s, intToBuffer(signature.v - 27)]) - const extraData = Buffer.concat([block.header.cliqueExtraVanity(), signatureB]) + extraData = Buffer.concat([extraData.slice(0, extraData.length - 65), signatureB]) blockData.header.extraData = extraData - block = Block.fromBlockData(blockData, { common: COMMON, freeze: false }) + block = Block.fromBlockData(blockData, { common, freeze: false }) await blockchain.putBlock(block) blocks.push(block) @@ -171,6 +191,7 @@ tape('Clique: Initialization', (t) => { t.test('should throw if signer in epoch checkpoint is not active', async (st) => { const { blockchain } = await initWithSigners([A]) ;(blockchain as any)._validateBlocks = false + ;(blockchain as any)._validateConsensus = false const number = COMMON.consensusConfig().epoch const unauthorizedSigner = Address.fromString('0x00a839de7922491683f547a67795204763ff8237') const extraData = Buffer.concat([ @@ -433,32 +454,176 @@ tape('Clique: Initialization', (t) => { st.end() }) - // TODO: fix test case - /*t.test('Clique Voting: Changes reaching consensus out of bounds (via a deauth) execute on touch', async (st) => { - const { blocks, blockchain } = await initWithSigners([A, B, C, D]) - await addNextBlock(blockchain, blocks, A, [C, false]) - await addNextBlock(blockchain, blocks, B) - await addNextBlock(blockchain, blocks, C) - await addNextBlock(blockchain, blocks, A, [D, false]) - await addNextBlock(blockchain, blocks, B, [C, false]) - await addNextBlock(blockchain, blocks, C) - await addNextBlock(blockchain, blocks, A) - await addNextBlock(blockchain, blocks, B, [D, false]) - await addNextBlock(blockchain, blocks, C, [D, false]) - await addNextBlock(blockchain, blocks, A) - await addNextBlock(blockchain, blocks, C, [C, true]) + t.test( + 'Clique Voting: Changes reaching consensus out of bounds (via a deauth) execute on touch', + async (st) => { + const { blocks, blockchain } = await initWithSigners([A, B, C, D]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A, [D, false]) + await addNextBlock(blockchain, blocks, B, [C, false]) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [D, false]) + await addNextBlock(blockchain, blocks, C, [D, false]) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, C, [C, true]) + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) + st.end() + } + ) - st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) + t.test( + 'Clique Voting: Changes reaching consensus out of bounds (via a deauth) may go out of consensus on first touch', + async (st) => { + const { blocks, blockchain } = await initWithSigners([A, B, C, D]) + await addNextBlock(blockchain, blocks, A, [C, false]) + await addNextBlock(blockchain, blocks, B) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A, [D, false]) + await addNextBlock(blockchain, blocks, B, [C, false]) + await addNextBlock(blockchain, blocks, C) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [D, false]) + await addNextBlock(blockchain, blocks, C, [D, false]) + await addNextBlock(blockchain, blocks, A) + await addNextBlock(blockchain, blocks, B, [C, true]) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address, C.address]) + st.end() + } + ) + + t.test( + "Clique Voting: Ensure that pending votes don't survive authorization status changes", + async (st) => { + // This corner case can only appear if a signer is quickly added, removed + // and then readded (or the inverse), while one of the original voters + // dropped. If a past vote is left cached in the system somewhere, this + // will interfere with the final signer outcome. + const { blocks, blockchain } = await initWithSigners([A, B, C, D, E]) + await addNextBlock(blockchain, blocks, A, [F, true]) // Authorize F, 3 votes needed + await addNextBlock(blockchain, blocks, B, [F, true]) + await addNextBlock(blockchain, blocks, C, [F, true]) + await addNextBlock(blockchain, blocks, D, [F, false]) // Deauthorize F, 4 votes needed (leave A's previous vote "unchanged") + await addNextBlock(blockchain, blocks, E, [F, false]) + await addNextBlock(blockchain, blocks, B, [F, false]) + await addNextBlock(blockchain, blocks, C, [F, false]) + await addNextBlock(blockchain, blocks, D, [F, true]) // Almost authorize F, 2/3 votes needed + await addNextBlock(blockchain, blocks, E, [F, true]) + await addNextBlock(blockchain, blocks, B, [A, false]) // Deauthorize A, 3 votes needed + await addNextBlock(blockchain, blocks, C, [A, false]) + await addNextBlock(blockchain, blocks, D, [A, false]) + await addNextBlock(blockchain, blocks, B, [F, true]) // Finish authorizing F, 3/3 votes needed + + st.deepEqual(blockchain.cliqueActiveSigners(), [ + B.address, + C.address, + D.address, + E.address, + F.address, + ]) + st.end() + } + ) + + t.test( + 'Clique Voting: Epoch transitions reset all votes to allow chain checkpointing', + async (st) => { + const common = Common.forCustomChain( + 'rinkeby', + { + consensus: { + type: 'poa', + algorithm: 'clique', + clique: { + period: 15, + epoch: 3, + }, + }, + }, + 'chainstart' + ) + const { blocks, blockchain } = await initWithSigners([A, B], common) + await addNextBlock(blockchain, blocks, A, [C, true], undefined, common) + await addNextBlock(blockchain, blocks, B, undefined, undefined, common) + await addNextBlock(blockchain, blocks, A, undefined, [A, B], common) + await addNextBlock(blockchain, blocks, B, [C, true], undefined, common) + + st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address]) + st.end() + } + ) + + t.test('Clique Voting: An unauthorized signer should not be able to sign blocks', async (st) => { + const { blocks, blockchain } = await initWithSigners([A]) + await addNextBlock(blockchain, blocks, A) + try { + await addNextBlock(blockchain, blocks, B) + st.fail('should throw error') + } catch (error) { + if (error.message.includes('invalid PoA block signature (clique)')) { + st.pass('correct error thrown') + } else { + st.fail('correct error not thrown') + } + } st.end() }) - // TODO: add two additional test cases, further last test cases not relevant yet - t.test('Clique Voting: ', async (st) => { - const { blocks, blockchain } = await initWithSigners([A]) - await addNextBlock(blockchain, blocks, A, [B, true]) + t.test( + 'Clique Voting: An authorized signer that signed recenty should not be able to sign again', + async (st) => { + const { blocks, blockchain } = await initWithSigners([A, B]) + await addNextBlock(blockchain, blocks, A) + try { + await addNextBlock(blockchain, blocks, A) + st.fail('should throw error') + } catch (error) { + if (error.message.includes('recently signed')) { + st.pass('correct error thrown') + } else { + st.fail('correct error not thrown') + } + } + st.end() + } + ) - st.deepEqual(blockchain.cliqueActiveSigners(), [A.address, B.address], '') - st.end() - })*/ + t.test( + 'Clique Voting: Recent signatures should not reset on checkpoint blocks imported in a batch', + async (st) => { + const common = Common.forCustomChain( + 'rinkeby', + { + consensus: { + type: 'poa', + algorithm: 'clique', + clique: { + period: 15, + epoch: 3, + }, + }, + }, + 'chainstart' + ) + const { blocks, blockchain } = await initWithSigners([A, B, C], common) + await addNextBlock(blockchain, blocks, A, undefined, undefined, common) + await addNextBlock(blockchain, blocks, B, undefined, undefined, common) + await addNextBlock(blockchain, blocks, A, undefined, [A, B, C], common) + try { + await addNextBlock(blockchain, blocks, A, undefined, undefined, common) + st.fail('should throw error') + } catch (error) { + if (error.message.includes('recently signed')) { + st.pass('correct error thrown') + } else { + st.fail('correct error not thrown') + } + } + st.end() + } + ) }) diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 7872a74bde..017df165a5 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -27,6 +27,10 @@ export interface Chain { consensus?: { type: string algorithm: string + clique: { + period: number + epoch: number + } } } From 6cbebb3727423cb5ca4242699184168e1c3f1669 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 27 Jan 2021 15:41:04 -0800 Subject: [PATCH 47/50] move clique constants to dedicated file --- packages/block/src/clique.ts | 11 +++++++++++ packages/block/src/header.ts | 17 ++++++++++------- packages/blockchain/src/clique.ts | 11 ++++++++--- 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 packages/block/src/clique.ts diff --git a/packages/block/src/clique.ts b/packages/block/src/clique.ts new file mode 100644 index 0000000000..5fda0f0caa --- /dev/null +++ b/packages/block/src/clique.ts @@ -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) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index b46659ad07..1e75159775 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -14,15 +14,15 @@ import { 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')) -const CLIQUE_EXTRA_VANITY = 32 -const CLIQUE_EXTRA_SEAL = 65 - -const CLIQUE_DIFF_INTURN = new BN(2) -const CLIQUE_DIFF_NOTURN = new BN(1) - /** * An object that represents the block header. */ @@ -562,7 +562,10 @@ export class BlockHeader { } } - /* Hash for PoA clique blocks is created without the seal */ + /** + * 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) diff --git a/packages/blockchain/src/clique.ts b/packages/blockchain/src/clique.ts index c701320e59..93c25d01be 100644 --- a/packages/blockchain/src/clique.ts +++ b/packages/blockchain/src/clique.ts @@ -1,13 +1,18 @@ import { Address, BN } from 'ethereumjs-util' -export type CliqueSignerState = [BN, Address[]] // [blockNumber, signers] +// Clique Signer State: [blockNumber, signers] +export type CliqueSignerState = [BN, Address[]] export type CliqueLatestSignerStates = CliqueSignerState[] -export type CliqueVote = [BN, [Address, Address, Buffer]] // [blockNumber, [signer, beneficiary, cliqueNonce]] +// Clique Vote: [blockNumber, [signer, beneficiary, cliqueNonce]] +export type CliqueVote = [BN, [Address, Address, Buffer]] export type CliqueLatestVotes = CliqueVote[] -export type CliqueBlockSigner = [BN, Address] // [blockNumber, signer] +// Clique Block Signer: [blockNumber, signer] +export type CliqueBlockSigner = [BN, Address] export type CliqueLatestBlockSigners = CliqueBlockSigner[] +// Magic nonce number to vote on adding a new signer export const CLIQUE_NONCE_AUTH = Buffer.from('ffffffffffffffff', 'hex') +// Magic nonce number to vote on removing a signer. export const CLIQUE_NONCE_DROP = Buffer.alloc(8) From 8c81c6978c640f49f43942f021e56ff63b07f555 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 27 Jan 2021 22:46:18 -0800 Subject: [PATCH 48/50] 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() + } + ) }) From 141159c1b27ccc9501843ff0556677bad0fca24b Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 27 Jan 2021 22:50:18 -0800 Subject: [PATCH 49/50] update package-lock --- package-lock.json | 837 ++++++++++++++++++++++++---------------------- 1 file changed, 442 insertions(+), 395 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0c9f1c1803..35c0a9a54b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -756,6 +756,15 @@ "path-exists": "^3.0.0" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -1968,21 +1977,21 @@ } }, "@octokit/auth-token": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz", - "integrity": "sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz", + "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==", "dev": true, "requires": { - "@octokit/types": "^6.0.0" + "@octokit/types": "^6.0.3" } }, "@octokit/endpoint": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.10.tgz", - "integrity": "sha512-9+Xef8nT7OKZglfkOMm7IL6VwxXUQyR7DUSU0LH/F7VNqs8vyd7es5pTfz9E7DwUIx7R3pGscxu1EBhYljyu7Q==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz", + "integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==", "dev": true, "requires": { - "@octokit/types": "^6.0.0", + "@octokit/types": "^6.0.3", "is-plain-object": "^5.0.0", "universal-user-agent": "^6.0.0" }, @@ -2002,9 +2011,9 @@ } }, "@octokit/openapi-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz", - "integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-3.3.0.tgz", + "integrity": "sha512-s3dd32gagPmKaSLNJ9aPNok7U+tl69YLESf6DgQz5Ml/iipPZtif3GLvWpNXoA6qspFm1LFUZX+C3SqWX/Y/TQ==", "dev": true }, "@octokit/plugin-enterprise-rest": { @@ -2034,9 +2043,9 @@ } }, "@octokit/plugin-request-log": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz", - "integrity": "sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz", + "integrity": "sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ==", "dev": true }, "@octokit/plugin-rest-endpoint-methods": { @@ -2061,9 +2070,9 @@ } }, "@octokit/request": { - "version": "5.4.12", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.12.tgz", - "integrity": "sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg==", + "version": "5.4.13", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.13.tgz", + "integrity": "sha512-WcNRH5XPPtg7i1g9Da5U9dvZ6YbTffw9BN2rVezYiE7couoSyaRsw0e+Tl8uk1fArHE7Dn14U7YqUDy59WaqEw==", "dev": true, "requires": { "@octokit/endpoint": "^6.0.1", @@ -2077,12 +2086,12 @@ }, "dependencies": { "@octokit/request-error": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.4.tgz", - "integrity": "sha512-LjkSiTbsxIErBiRh5wSZvpZqT4t0/c9+4dOe0PII+6jXR+oj/h66s7E4a/MghV7iT8W9ffoQ5Skoxzs96+gBPA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz", + "integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==", "dev": true, "requires": { - "@octokit/types": "^6.0.0", + "@octokit/types": "^6.0.3", "deprecation": "^2.0.0", "once": "^1.4.0" } @@ -2148,21 +2157,21 @@ } }, "@octokit/types": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz", - "integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.6.0.tgz", + "integrity": "sha512-nmFoU3HCbw1AmnZU/eto2VvzV06+N7oAqXwMmAHGlNDF+KFykksh/VlAl85xc1P5T7Mw8fKYvXNaImNHCCH/rg==", "dev": true, "requires": { - "@octokit/openapi-types": "^2.0.0", + "@octokit/openapi-types": "^3.3.0", "@types/node": ">= 8" } }, "@polkadot/ts": { - "version": "0.3.58", - "resolved": "https://registry.npmjs.org/@polkadot/ts/-/ts-0.3.58.tgz", - "integrity": "sha512-gWAbFlaXbohNq89RvXuu6QZfot1pKPUZiQrUwX13VfTXhGfIJ29Dtc62f4KkUxNs6pHbiTti6tFocnPCPWlCVQ==", + "version": "0.3.59", + "resolved": "https://registry.npmjs.org/@polkadot/ts/-/ts-0.3.59.tgz", + "integrity": "sha512-opTBp1r2UpvTKl2ZbxodnYIUZMjsR6wjnXV87s7VUl+cyUiJhNRh2wpeZ5jXbeUAdYPXGh/8h/hu1WfyKvnROA==", "requires": { - "@types/chrome": "^0.0.126" + "@types/chrome": "^0.0.127" } }, "@types/abstract-leveldown": { @@ -2205,9 +2214,9 @@ } }, "@types/chrome": { - "version": "0.0.126", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.126.tgz", - "integrity": "sha512-191z7uoyfbGU+z7/m45j9XbWugWqVHVPMM4hJV5cZ+3YzGCT9wFjMUHO3Wr3Xvo8aVodvRNu28u7lvEaAnfbzg==", + "version": "0.0.127", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.127.tgz", + "integrity": "sha512-hBB9EApLYKKn2GvklVkTxVP6vZvxsH9okyIRUinNtMzZHIgIKWQk/ESbX+O5g4Bihfy38+aFGn7Kl7Cxou5JUg==", "requires": { "@types/filesystem": "*", "@types/har-format": "*" @@ -2237,9 +2246,9 @@ "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==" }, "@types/express-serve-static-core": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz", - "integrity": "sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==", + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz", + "integrity": "sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA==", "requires": { "@types/node": "*", "@types/qs": "*", @@ -2283,9 +2292,9 @@ } }, "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, "@types/k-bucket": { "version": "5.0.0", @@ -2313,9 +2322,9 @@ } }, "@types/lodash": { - "version": "4.14.167", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.167.tgz", - "integrity": "sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw==" + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==" }, "@types/lru-cache": { "version": "5.1.0", @@ -2338,9 +2347,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "@types/node": { - "version": "11.15.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.42.tgz", - "integrity": "sha512-CUwq20cDEavWvtzSQVcBKjG6bj/JiBMGJqar7uNPQR+JUbpPg6Pvjk/piV1YTJzCck8yD2SZQOmnJ3ZvOkq4Mg==" + "version": "11.15.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.44.tgz", + "integrity": "sha512-fHAcNn2FlzDMA9rV5KP73r1cMZdbommWic2foHAEWoa/LITi91AueHDsJpnqjBEJ7bYoT2WC+KN1RL0vsM20zA==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -2397,14 +2406,15 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.11.1.tgz", - "integrity": "sha512-fABclAX2QIEDmTMk6Yd7Muv1CzFLwWM4505nETzRHpP3br6jfahD9UUJkhnJ/g2m7lwfz8IlswcwGGPGiq9exw==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.1.tgz", + "integrity": "sha512-5JriGbYhtqMS1kRcZTQxndz1lKMwwEXKbwZbkUZNnp6MJX0+OVXnG0kOlBZP4LUAxEyzu3cs+EXd/97MJXsGfw==", "requires": { - "@typescript-eslint/experimental-utils": "4.11.1", - "@typescript-eslint/scope-manager": "4.11.1", + "@typescript-eslint/experimental-utils": "4.14.1", + "@typescript-eslint/scope-manager": "4.14.1", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", "regexpp": "^3.0.0", "semver": "^7.3.2", "tsutils": "^3.17.1" @@ -2416,25 +2426,25 @@ "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" }, "@typescript-eslint/experimental-utils": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.11.1.tgz", - "integrity": "sha512-mAlWowT4A6h0TC9F+J5pdbEhjNiEMO+kqPKQ4sc3fVieKL71dEqfkKgtcFVSX3cjSBwYwhImaQ/mXQF0oaI38g==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.1.tgz", + "integrity": "sha512-2CuHWOJwvpw0LofbyG5gvYjEyoJeSvVH2PnfUQSn0KQr4v8Dql2pr43ohmx4fdPQ/eVoTSFjTi/bsGEXl/zUUQ==", "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.11.1", - "@typescript-eslint/types": "4.11.1", - "@typescript-eslint/typescript-estree": "4.11.1", + "@typescript-eslint/scope-manager": "4.14.1", + "@typescript-eslint/types": "4.14.1", + "@typescript-eslint/typescript-estree": "4.14.1", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/typescript-estree": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.1.tgz", - "integrity": "sha512-tC7MKZIMRTYxQhrVAFoJq/DlRwv1bnqA4/S2r3+HuHibqvbrPcyf858lNzU7bFmy4mLeIHFYr34ar/1KumwyRw==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.1.tgz", + "integrity": "sha512-M8+7MbzKC1PvJIA8kR2sSBnex8bsR5auatLCnVlNTJczmJgqRn8M+sAlQfkEq7M4IY3WmaNJ+LJjPVRrREVSHQ==", "requires": { - "@typescript-eslint/types": "4.11.1", - "@typescript-eslint/visitor-keys": "4.11.1", + "@typescript-eslint/types": "4.14.1", + "@typescript-eslint/visitor-keys": "4.14.1", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -2473,9 +2483,9 @@ } }, "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2494,9 +2504,9 @@ } }, "globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", + "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -2584,18 +2594,18 @@ } }, "@typescript-eslint/scope-manager": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.11.1.tgz", - "integrity": "sha512-Al2P394dx+kXCl61fhrrZ1FTI7qsRDIUiVSuN6rTwss6lUn8uVO2+nnF4AvO0ug8vMsy3ShkbxLu/uWZdTtJMQ==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.14.1.tgz", + "integrity": "sha512-F4bjJcSqXqHnC9JGUlnqSa3fC2YH5zTtmACS1Hk+WX/nFB0guuynVK5ev35D4XZbdKjulXBAQMyRr216kmxghw==", "requires": { - "@typescript-eslint/types": "4.11.1", - "@typescript-eslint/visitor-keys": "4.11.1" + "@typescript-eslint/types": "4.14.1", + "@typescript-eslint/visitor-keys": "4.14.1" } }, "@typescript-eslint/types": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.11.1.tgz", - "integrity": "sha512-5kvd38wZpqGY4yP/6W3qhYX6Hz0NwUbijVsX2rxczpY6OXaMxh0+5E5uLJKVFwaBM7PJe1wnMym85NfKYIh6CA==" + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.14.1.tgz", + "integrity": "sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w==" }, "@typescript-eslint/typescript-estree": { "version": "2.34.0", @@ -2648,11 +2658,11 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.1.tgz", - "integrity": "sha512-IrlBhD9bm4bdYcS8xpWarazkKXlE7iYb1HzRuyBP114mIaj5DJPo11Us1HgH60dTt41TCZXMaTCAW+OILIYPOg==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz", + "integrity": "sha512-TAblbDXOI7bd0C/9PE1G+AFo7R5uc+ty1ArDoxmrC1ah61Hn6shURKy7gLdRb1qKJmjHkqu5Oq+e4Kt0jwf1IA==", "requires": { - "@typescript-eslint/types": "4.11.1", + "@typescript-eslint/types": "4.14.1", "eslint-visitor-keys": "^2.0.0" }, "dependencies": { @@ -3290,11 +3300,6 @@ "async": "^2.4.0" } }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3526,14 +3531,6 @@ "platform": "^1.3.3" } }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "requires": { - "callsite": "1.0.0" - } - }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3545,9 +3542,9 @@ "integrity": "sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==" }, "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bindings": { "version": "1.5.0", @@ -4044,12 +4041,12 @@ } }, "call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "requires": { "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" + "get-intrinsic": "^1.0.2" } }, "call-me-maybe": { @@ -4084,11 +4081,6 @@ "caller-callsite": "^2.0.0" } }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4146,13 +4138,13 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "chokidar": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", - "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", "requires": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.1.2", + "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -4652,9 +4644,9 @@ "dev": true }, "conventional-changelog-writer": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.18.tgz", - "integrity": "sha512-mAQDCKyB9HsE8Ko5cCM1Jn1AWxXPYV0v8dFPabZRkvsiWUul2YyAqbIaoMKF88Zf2ffnOPSvKhboLf3fnjo5/A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz", + "integrity": "sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw==", "dev": true, "requires": { "compare-func": "^2.0.0", @@ -4869,9 +4861,9 @@ "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" }, "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookiejar": { "version": "2.1.2", @@ -4902,9 +4894,9 @@ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" }, "core-js-pure": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.1.tgz", - "integrity": "sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g==" + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.3.tgz", + "integrity": "sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA==" }, "core-util-is": { "version": "1.0.2", @@ -5068,9 +5060,12 @@ } }, "d3-array": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", - "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz", + "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==", + "requires": { + "internmap": "^1.0.0" + } }, "d3-color": { "version": "2.0.0", @@ -5764,16 +5759,16 @@ } }, "engine.io": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", - "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", + "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "0.3.1", + "cookie": "~0.4.1", "debug": "~4.1.0", "engine.io-parser": "~2.2.0", - "ws": "^7.1.2" + "ws": "~7.4.2" }, "dependencies": { "debug": { @@ -5792,9 +5787,9 @@ } }, "engine.io-client": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", - "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz", + "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==", "requires": { "component-emitter": "~1.3.0", "component-inherit": "0.0.3", @@ -5804,7 +5799,7 @@ "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~6.1.0", + "ws": "~7.4.2", "xmlhttprequest-ssl": "~1.5.4", "yeast": "0.1.2" }, @@ -5821,24 +5816,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" - }, - "parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" - }, - "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", - "requires": { - "async-limiter": "~1.0.0" - } } } }, @@ -5855,9 +5832,9 @@ } }, "enhanced-resolve": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", - "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", "requires": { "graceful-fs": "^4.1.2", "memory-fs": "^0.5.0", @@ -5908,22 +5885,24 @@ } }, "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "version": "1.18.0-next.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", + "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", "requires": { + "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2", "has": "^1.0.3", "has-symbols": "^1.0.1", "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", + "object-inspect": "^1.9.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.3", + "string.prototype.trimstart": "^1.0.3" } }, "es-to-primitive": { @@ -6504,9 +6483,9 @@ } }, "eslint-plugin-prettier": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz", - "integrity": "sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", + "integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", "requires": { "prettier-linter-helpers": "^1.0.0" } @@ -7116,6 +7095,14 @@ "semver": "^5.6.0" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -7150,21 +7137,22 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "requires": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "find-versions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", - "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", + "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", "dev": true, "requires": { - "semver-regex": "^2.0.0" + "semver-regex": "^3.1.2" } }, "findup": { @@ -7253,9 +7241,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", + "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" }, "for-each": { "version": "0.3.3", @@ -7425,9 +7413,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", + "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "optional": true }, "fsm": { @@ -7520,9 +7508,9 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.0.tgz", + "integrity": "sha512-M11rgtQp5GZMZzDL7jLTNxbDfurpzuau5uqRWDPvlHjfvg3TdScAZo96GLvhMjImrmR8uAt0FS2RLoMrfWGKlg==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -7965,9 +7953,9 @@ } }, "git-url-parse": { - "version": "11.4.3", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.3.tgz", - "integrity": "sha512-LZTTk0nqJnKN48YRtOpR8H5SEfp1oM2tls90NuZmBxN95PnCvmuXGzqQ4QmVirBgKx2KPYfPGteX3/raWjKenQ==", + "version": "11.4.4", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz", + "integrity": "sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw==", "dev": true, "requires": { "git-up": "^4.0.0" @@ -8447,18 +8435,18 @@ } }, "husky": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.6.tgz", - "integrity": "sha512-o6UjVI8xtlWRL5395iWq9LKDyp/9TE7XMOTvIpEVzW638UcGxTmV5cfel6fsk/jbZSTlvfGVJf2svFtybcIZag==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", + "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", "dev": true, "requires": { "chalk": "^4.0.0", "ci-info": "^2.0.0", "compare-versions": "^3.6.0", "cosmiconfig": "^7.0.0", - "find-versions": "^3.2.0", + "find-versions": "^4.0.0", "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^4.2.0", + "pkg-dir": "^5.0.0", "please-upgrade-node": "^3.2.0", "slash": "^3.0.0", "which-pm-runs": "^1.0.0" @@ -8572,9 +8560,9 @@ "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" }, "import-fresh": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", - "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -8606,6 +8594,14 @@ "path-exists": "^3.0.0" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -8862,6 +8858,11 @@ "uuid": "^3.2.2" } }, + "internmap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.0.tgz", + "integrity": "sha512-SdoDWwNOTE2n4JWUsLn4KXZGuZPjPF9yyOGc8bnfWnBQh7BD/l80rzSznKc/r4Y0aQ7z3RTk9X+tV4tHBpu+dA==" + }, "interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -9485,9 +9486,9 @@ }, "dependencies": { "@types/node": { - "version": "12.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.11.tgz", - "integrity": "sha512-bwVfNTFZOrGXyiQ6t4B9sZerMSShWNsGRw8tC5DY1qImUNczS9SjT4G6PnzjCnxsu5Ubj6xjL2lgwddkxtQl5w==" + "version": "12.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.15.tgz", + "integrity": "sha512-lowukE3GUI+VSYSu6VcBXl14d61Rp5hA1D+61r16qnwC0lYNSqdxcvRh0pswejorHfS+HgwBasM8jLXz0/aOsw==" }, "commander": { "version": "2.20.3", @@ -9763,6 +9764,15 @@ "to-regex-range": "^5.0.1" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -9773,6 +9783,30 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -9938,14 +9972,14 @@ }, "dependencies": { "acorn": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz", - "integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==" + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz", + "integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==" }, "acorn-walk": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.0.tgz", - "integrity": "sha512-oZRad/3SMOI/pxbbmqyurIx7jHw1wZDcR9G44L8pUVFEomX/0dH89SrM1KaDXuv1NpzAXz6Op/Xu/Qd5XXzdEA==" + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.2.tgz", + "integrity": "sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==" }, "assert": { "version": "2.0.0", @@ -10946,11 +10980,12 @@ } }, "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "requires": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" } }, "lodash": { @@ -11324,9 +11359,9 @@ } }, "meow": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.0.0.tgz", - "integrity": "sha512-nbsTRz2fwniJBFgUkcdISq8y/q9n9VbiHYbfwklFh5V4V2uAcxtKQkDc0yCLPM/kP0d+inZBewn3zJqewHE7kg==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", "dev": true, "requires": { "@types/minimist": "^1.2.0", @@ -11342,6 +11377,16 @@ "yargs-parser": "^20.2.3" }, "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "hosted-git-info": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.7.tgz", @@ -11351,6 +11396,15 @@ "lru-cache": "^6.0.0" } }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -11372,6 +11426,24 @@ "validate-npm-package-license": "^3.0.1" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -11527,21 +11599,21 @@ } }, "mime": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", - "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz", + "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==" }, "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", "requires": { - "mime-db": "1.44.0" + "mime-db": "1.45.0" } }, "mimic-fn": { @@ -11788,9 +11860,9 @@ } }, "ip-regex": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.2.0.tgz", - "integrity": "sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" }, "is-ip": { "version": "3.1.0", @@ -12579,6 +12651,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -12632,11 +12712,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -12859,19 +12934,21 @@ "dev": true }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "requires": { - "p-limit": "^2.2.0" + "p-limit": "^3.0.2" } }, "p-map": { @@ -13014,9 +13091,9 @@ "dev": true }, "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -13036,13 +13113,23 @@ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, "parse-path": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.2.tgz", - "integrity": "sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-4.0.3.tgz", + "integrity": "sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA==", "dev": true, "requires": { "is-ssh": "^1.3.0", - "protocols": "^1.4.0" + "protocols": "^1.4.0", + "qs": "^6.9.4", + "query-string": "^6.13.8" + }, + "dependencies": { + "qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==", + "dev": true + } } }, "parse-url": { @@ -13058,20 +13145,14 @@ } }, "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" }, "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" }, "parseurl": { "version": "1.3.3", @@ -13366,12 +13447,12 @@ } }, "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", "dev": true, "requires": { - "find-up": "^4.0.0" + "find-up": "^5.0.0" } }, "pkg-up": { @@ -13576,9 +13657,9 @@ "dev": true }, "protocol-buffers-schema": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.4.0.tgz", - "integrity": "sha512-G/2kcamPF2S49W5yaMGdIpkG6+5wZF0fzBteLKgEHjbNzqjZQ85aAs1iJGto31EJaSTkNvHs5IXuHSaTLWBAiA==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz", + "integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw==" }, "protocols": { "version": "1.4.8", @@ -13792,6 +13873,17 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "query-string": { + "version": "6.13.8", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.8.tgz", + "integrity": "sha512-jxJzQI2edQPE/NPUOusNjO/ZOGqr1o2OBa/3M00fU76FsLXDVbJDv/p7ng5OdQyorKrkRz1oqfwmbe5MAMePQg==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -14126,32 +14218,12 @@ } }, "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, "regexpp": { @@ -14372,9 +14444,9 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rfdc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", - "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.2.0.tgz", + "integrity": "sha512-ijLyszTMmUrXvjSooucVQwimGUk84eRcmCuLV8Xghe3UO85mjUtRAHRyoMM6XtyqbECaXuBWx18La3523sXINA==" }, "rimraf": { "version": "2.7.1", @@ -14528,9 +14600,9 @@ "dev": true }, "semver-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", - "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz", + "integrity": "sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==", "dev": true }, "serialize-javascript": { @@ -14826,15 +14898,15 @@ } }, "socket.io": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", - "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz", + "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==", "requires": { "debug": "~4.1.0", - "engine.io": "~3.4.0", + "engine.io": "~3.5.0", "has-binary2": "~1.0.2", "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.3.0", + "socket.io-client": "2.4.0", "socket.io-parser": "~3.4.0" }, "dependencies": { @@ -14859,42 +14931,29 @@ "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" }, "socket.io-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", - "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", + "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", "requires": { "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "engine.io-client": "~3.4.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", "has-binary2": "~1.0.2", - "has-cors": "1.1.0", "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", + "parseqs": "0.0.6", + "parseuri": "0.0.6", "socket.io-parser": "~3.3.0", "to-array": "0.1.4" }, "dependencies": { - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { - "ms": "^2.1.1" + "ms": "2.0.0" } }, "isarray": { @@ -14903,38 +14962,18 @@ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "socket.io-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", - "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", + "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", "requires": { "component-emitter": "~1.3.0", "debug": "~3.1.0", "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } } } } @@ -15121,6 +15160,12 @@ "through": "2" } }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "dev": true + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -15686,6 +15731,12 @@ } } }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", + "dev": true + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -16225,6 +16276,14 @@ "path-exists": "^3.0.0" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -16661,9 +16720,9 @@ } }, "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.20.0.tgz", + "integrity": "sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==", "requires": { "tslib": "^1.8.1" } @@ -16786,14 +16845,14 @@ }, "dependencies": { "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "requires": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", - "universalify": "^1.0.0" + "universalify": "^2.0.0" } }, "jsonfile": { @@ -16803,19 +16862,12 @@ "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - } } }, "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" } } }, @@ -16839,9 +16891,9 @@ "integrity": "sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==" }, "uglify-js": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.2.tgz", - "integrity": "sha512-rWYleAvfJPjduYCt+ELvzybNah/zIkRteGXIBO8X0lteRZPGladF61hFi8tU7qKTsF7u6DUQCtT9k00VlFOgkg==", + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.5.tgz", + "integrity": "sha512-SgpgScL4T7Hj/w/GexjnBHi3Ien9WS1Rpfg5y91WXMj9SY997ZCQU76mH4TpLwwfmMvoOU8wiaRkIf6NaH3mtg==", "optional": true }, "uid-number": { @@ -16986,9 +17038,9 @@ "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" }, "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "requires": { "punycode": "^2.1.0" } @@ -17070,34 +17122,15 @@ } }, "util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.1.tgz", + "integrity": "sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==", "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", + "for-each": "^0.3.3", "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } + "object.getownpropertydescriptors": "^2.1.1" } }, "utils-merge": { @@ -17315,9 +17348,9 @@ "dev": true }, "webpack": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", - "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", + "version": "4.46.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", + "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -17327,7 +17360,7 @@ "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.3.0", + "enhanced-resolve": "^4.5.0", "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", "loader-runner": "^2.4.0", @@ -17859,6 +17892,14 @@ "path-exists": "^3.0.0" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -17929,6 +17970,12 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } From 6721321fb46e8543eeb1027d11fdd8cdf8841994 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 27 Jan 2021 23:17:50 -0800 Subject: [PATCH 50/50] fix vm test (use pow chain instead of poa chain, was throwing on signature validation) --- packages/vm/tests/api/runBlockchain.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vm/tests/api/runBlockchain.spec.ts b/packages/vm/tests/api/runBlockchain.spec.ts index 18b94f1697..c0a5d7396c 100644 --- a/packages/vm/tests/api/runBlockchain.spec.ts +++ b/packages/vm/tests/api/runBlockchain.spec.ts @@ -68,6 +68,7 @@ tape('runBlockchain', (t) => { }) t.test('should run with valid and invalid blocks', async (st) => { + const blockchainDB = level() // Produce error on the third time runBlock is called let runBlockInvocations = 0 ;(vm).runBlock = () => @@ -79,7 +80,7 @@ tape('runBlockchain', (t) => { resolve({}) }) - const common = new Common({ chain: 'goerli', hardfork: 'chainstart' }) + const common = new Common({ chain: 'mainnet', hardfork: 'chainstart' }) const genesisBlock = Block.genesis(undefined, { common }) const newBlockchain = new Blockchain({ db: blockchainDB,