@ethereumjs/blockchain
Version:
A module to store and interact with blocks
522 lines • 24.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CliqueConsensus = exports.CLIQUE_DIFF_NOTURN = exports.CLIQUE_DIFF_INTURN = exports.CLIQUE_NONCE_DROP = exports.CLIQUE_NONCE_AUTH = void 0;
const block_1 = require("@ethereumjs/block");
const common_1 = require("@ethereumjs/common");
const rlp_1 = require("@ethereumjs/rlp");
const util_1 = require("@ethereumjs/util");
const debug_1 = require("debug");
const debug = (0, debug_1.default)('blockchain:clique');
// Magic nonce number to vote on adding a new signer
exports.CLIQUE_NONCE_AUTH = (0, util_1.hexToBytes)('0xffffffffffffffff');
// Magic nonce number to vote on removing a signer.
exports.CLIQUE_NONCE_DROP = new Uint8Array(8);
const CLIQUE_SIGNERS_KEY = 'CliqueSigners';
const CLIQUE_VOTES_KEY = 'CliqueVotes';
const CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY = 'CliqueBlockSignersSnapshot';
// Block difficulty for in-turn signatures
exports.CLIQUE_DIFF_INTURN = util_1.BIGINT_2;
// Block difficulty for out-of-turn signatures
exports.CLIQUE_DIFF_NOTURN = util_1.BIGINT_1;
/**
* This class encapsulates Clique-related consensus functionality when used with the Blockchain class.
* Note: reorgs which happen between epoch transitions, which change the internal voting state over the reorg
* will result in failure and is currently not supported.
* The hotfix for this could be: re-load the latest epoch block (this has the clique state in the extraData of the header)
* Now replay all blocks on top of it. This should validate the chain up to the new/reorged tip which previously threw.
*/
class CliqueConsensus {
constructor() {
/**
* 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.
*/
this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 200;
/**
* 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 elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
*
* Always keep at least one item on the stack.
*/
this._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 elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
*/
this._cliqueLatestVotes = [];
/**
* List of signers for the last consecutive {@link Blockchain.cliqueSignerLimit} blocks.
* Kept as a snapshot for quickly checking for "recently signed" error.
* Format: [ [BLOCK_NUMBER, SIGNER_ADDRESS], ...]
*
* On reorgs elements from the array are removed until BLOCK_NUMBER > REORG_BLOCK.
*/
this._cliqueLatestBlockSigners = [];
// Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables
// Additional window check is to prevent vite browser bundling (and potentially other) to break
this.DEBUG =
typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false;
this.algorithm = common_1.ConsensusAlgorithm.Clique;
}
/**
*
* @param param dictionary containing a {@link Blockchain} object
*
* Note: this method must be called before consensus checks are used or type errors will occur
*/
async setup({ blockchain }) {
this.blockchain = blockchain;
this._cliqueLatestSignerStates = await this.getCliqueLatestSignerStates();
this._cliqueLatestSignerStates.sort((a, b) => (a[0] > b[0] ? 1 : -1));
this._cliqueLatestVotes = await this.getCliqueLatestVotes();
this._cliqueLatestBlockSigners = await this.getCliqueLatestBlockSigners();
}
async genesisInit(genesisBlock) {
await this.cliqueSaveGenesisSigners(genesisBlock);
}
async validateConsensus(block) {
if (!this.blockchain) {
throw (0, util_1.EthereumJSErrorWithoutCode)('blockchain not provided');
}
const { header } = block;
const valid = (0, block_1.cliqueVerifySignature)(header, this.cliqueActiveSigners(header.number));
if (!valid) {
throw (0, util_1.EthereumJSErrorWithoutCode)('invalid PoA block signature (clique)');
}
if (this.cliqueCheckRecentlySigned(header)) {
throw (0, util_1.EthereumJSErrorWithoutCode)('recently signed');
}
// validate checkpoint signers towards active signers on epoch transition blocks
if ((0, block_1.cliqueIsEpochTransition)(header)) {
// note: keep votes on epoch transition blocks in case of reorgs.
// only active (non-stale) votes will counted (if vote.blockNumber >= lastEpochBlockNumber
const checkpointSigners = (0, block_1.cliqueEpochTransitionSigners)(header);
const activeSigners = this.cliqueActiveSigners(header.number);
for (const [i, cSigner] of checkpointSigners.entries()) {
if (activeSigners[i]?.equals(cSigner) !== true) {
throw (0, util_1.EthereumJSErrorWithoutCode)(`checkpoint signer not found in active signers list at index ${i}: ${cSigner}`);
}
}
}
}
async validateDifficulty(header) {
if (!this.blockchain) {
throw (0, util_1.EthereumJSErrorWithoutCode)('blockchain not provided');
}
if (header.difficulty !== exports.CLIQUE_DIFF_INTURN && header.difficulty !== exports.CLIQUE_DIFF_NOTURN) {
const msg = `difficulty for clique block must be INTURN (2) or NOTURN (1), received: ${header.difficulty}`;
throw (0, util_1.EthereumJSErrorWithoutCode)(`${msg} ${header.errorStr()}`);
}
const signers = this.cliqueActiveSigners(header.number);
if (signers.length === 0) {
// abort if signers are unavailable
const msg = 'no signers available';
throw (0, util_1.EthereumJSErrorWithoutCode)(`${msg} ${header.errorStr()}`);
}
const signerIndex = signers.findIndex((address) => address.equals((0, block_1.cliqueSigner)(header)));
const inTurn = header.number % BigInt(signers.length) === BigInt(signerIndex);
if ((inTurn && header.difficulty === exports.CLIQUE_DIFF_INTURN) ||
(!inTurn && header.difficulty === exports.CLIQUE_DIFF_NOTURN)) {
return;
}
throw (0, util_1.EthereumJSErrorWithoutCode)(`'invalid clique difficulty ${header.errorStr()}`);
}
async newBlock(block, commonAncestor) {
// Clique: update signer votes and state
const { header } = block;
const commonAncestorNumber = commonAncestor?.number;
if (commonAncestorNumber !== undefined) {
await this._cliqueDeleteSnapshots(commonAncestorNumber + util_1.BIGINT_1);
for (let number = commonAncestorNumber + BigInt(1); number <= header.number; number++) {
const canonicalHeader = await this.blockchain.getCanonicalHeader(number);
await this._cliqueBuildSnapshots(canonicalHeader);
}
}
}
/**
* Save genesis signers to db
* @param genesisBlock genesis block
* @hidden
*/
async cliqueSaveGenesisSigners(genesisBlock) {
const genesisSignerState = [
util_1.BIGINT_0,
(0, block_1.cliqueEpochTransitionSigners)(genesisBlock.header),
];
await this.cliqueUpdateSignerStates(genesisSignerState);
this.DEBUG && debug(`[Block 0] Genesis block -> update signer states`);
await this.cliqueUpdateVotes();
}
/**
* Save signer state to db
* @param signerState
* @hidden
*/
async cliqueUpdateSignerStates(signerState) {
if (signerState) {
const blockNumber = signerState[0];
const known = this._cliqueLatestSignerStates.find((value) => {
if (value[0] === blockNumber) {
return true;
}
});
if (known !== undefined) {
return;
}
this._cliqueLatestSignerStates.push(signerState);
this._cliqueLatestSignerStates.sort((a, b) => (a[0] > b[0] ? 1 : -1));
}
// trim to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT;
const blockSigners = this._cliqueLatestBlockSigners;
const lastBlockNumber = blockSigners[blockSigners.length - 1]?.[0];
if (lastBlockNumber) {
const blockLimit = lastBlockNumber - BigInt(limit);
const states = this._cliqueLatestSignerStates;
const lastItem = states[states.length - 1];
this._cliqueLatestSignerStates = states.filter((state) => state[0] >= blockLimit);
if (this._cliqueLatestSignerStates.length === 0) {
// always keep at least one item on the stack
this._cliqueLatestSignerStates.push(lastItem);
}
}
// save to db
const formatted = this._cliqueLatestSignerStates.map((state) => [
(0, util_1.bigIntToBytes)(state[0]),
state[1].map((a) => a.toBytes()),
]);
await this.blockchain.db.put(CLIQUE_SIGNERS_KEY, rlp_1.RLP.encode(formatted));
// Output active signers for debugging purposes
if (signerState !== undefined) {
let i = 0;
try {
for (const signer of this.cliqueActiveSigners(signerState[0])) {
this.DEBUG && debug(`Clique signer [${i}]: ${signer} (block: ${signerState[0]})`);
i++;
}
// eslint-disable-next-line no-empty
}
catch { }
}
}
/**
* Update clique votes and save to db
* @param header BlockHeader
* @hidden
*/
async cliqueUpdateVotes(header) {
// Block contains a vote on a new signer
if (header && !header.coinbase.isZero()) {
const signer = (0, block_1.cliqueSigner)(header);
const beneficiary = header.coinbase;
const nonce = header.nonce;
const latestVote = [header.number, [signer, beneficiary, nonce]];
// Do two rounds here, one to execute on a potential previously reached consensus
// on the newly touched beneficiary, one with the added new vote
for (let round = 1; round <= 2; round++) {
// See if there is a new majority consensus to update the signer list
const lastEpochBlockNumber = header.number -
(header.number %
BigInt(this.blockchain.common.consensusConfig().epoch));
const limit = this.cliqueSignerLimit(header.number);
let activeSigners = [...this.cliqueActiveSigners(header.number)];
let consensus = false;
// AUTH vote analysis
let votes = this._cliqueLatestVotes.filter((vote) => {
return (vote[0] >= BigInt(lastEpochBlockNumber) &&
!vote[1][0].equals(signer) &&
vote[1][1].equals(beneficiary) &&
(0, util_1.equalsBytes)(vote[1][2], exports.CLIQUE_NONCE_AUTH));
});
const beneficiaryVotesAUTH = [];
for (const vote of votes) {
const num = beneficiaryVotesAUTH.filter((voteCMP) => {
return voteCMP.equals(vote[1][0]);
}).length;
if (num === 0) {
beneficiaryVotesAUTH.push(vote[1][0]);
}
}
let numBeneficiaryVotesAUTH = beneficiaryVotesAUTH.length;
if (round === 2 && (0, util_1.equalsBytes)(nonce, exports.CLIQUE_NONCE_AUTH)) {
numBeneficiaryVotesAUTH += 1;
}
// Majority consensus
if (numBeneficiaryVotesAUTH >= limit) {
consensus = true;
// Authorize new signer
activeSigners.push(beneficiary);
activeSigners.sort((a, b) => {
// Sort by array size
const result = (0, util_1.toType)(a.toString(), util_1.TypeOutput.BigInt) < (0, util_1.toType)(b.toString(), util_1.TypeOutput.BigInt);
if (result) {
return -1;
}
else {
return 1;
}
});
// Discard votes for added signer
this._cliqueLatestVotes = this._cliqueLatestVotes.filter((vote) => !vote[1][1].equals(beneficiary));
this.DEBUG &&
debug(`[Block ${header.number}] Clique majority consensus (AUTH ${beneficiary})`);
}
// DROP vote
votes = this._cliqueLatestVotes.filter((vote) => {
return (vote[0] >= BigInt(lastEpochBlockNumber) &&
!vote[1][0].equals(signer) &&
vote[1][1].equals(beneficiary) &&
(0, util_1.equalsBytes)(vote[1][2], exports.CLIQUE_NONCE_DROP));
});
const beneficiaryVotesDROP = [];
for (const vote of votes) {
const num = beneficiaryVotesDROP.filter((voteCMP) => {
return voteCMP.equals(vote[1][0]);
}).length;
if (num === 0) {
beneficiaryVotesDROP.push(vote[1][0]);
}
}
let numBeneficiaryVotesDROP = beneficiaryVotesDROP.length;
if (round === 2 && (0, util_1.equalsBytes)(nonce, exports.CLIQUE_NONCE_DROP)) {
numBeneficiaryVotesDROP += 1;
}
// Majority consensus
if (numBeneficiaryVotesDROP >= limit) {
consensus = true;
// Drop signer
activeSigners = activeSigners.filter((signer) => !signer.equals(beneficiary));
this._cliqueLatestVotes = this._cliqueLatestVotes.filter(
// Discard votes from removed signer and for removed signer
(vote) => !vote[1][0].equals(beneficiary) && !vote[1][1].equals(beneficiary));
this.DEBUG &&
debug(`[Block ${header.number}] Clique majority consensus (DROP ${beneficiary})`);
}
if (round === 1) {
// Always add the latest vote to the history no matter if already voted
// the same vote or not
this._cliqueLatestVotes.push(latestVote);
this.DEBUG &&
debug(`[Block ${header.number}] New clique vote: ${signer} -> ${beneficiary} ${(0, util_1.equalsBytes)(nonce, exports.CLIQUE_NONCE_AUTH) ? 'AUTH' : 'DROP'}`);
}
if (consensus) {
if (round === 1) {
this.DEBUG &&
debug(`[Block ${header.number}] Clique majority consensus on existing votes -> update signer states`);
}
else {
this.DEBUG &&
debug(`[Block ${header.number}] Clique majority consensus on new vote -> update signer states`);
}
const newSignerState = [header.number, activeSigners];
await this.cliqueUpdateSignerStates(newSignerState);
return;
}
}
}
// trim to lastEpochBlockNumber - CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT;
const blockSigners = this._cliqueLatestBlockSigners;
const lastBlockNumber = blockSigners[blockSigners.length - 1]?.[0];
if (lastBlockNumber) {
const lastEpochBlockNumber = lastBlockNumber -
(lastBlockNumber %
BigInt(this.blockchain.common.consensusConfig().epoch));
const blockLimit = lastEpochBlockNumber - BigInt(limit);
this._cliqueLatestVotes = this._cliqueLatestVotes.filter((state) => state[0] >= blockLimit);
}
// save votes to db
const formatted = this._cliqueLatestVotes.map((v) => [
(0, util_1.bigIntToBytes)(v[0]),
[v[1][0].toBytes(), v[1][1].toBytes(), v[1][2]],
]);
await this.blockchain.db.put(CLIQUE_VOTES_KEY, rlp_1.RLP.encode(formatted));
}
/**
* Returns a list with the current block signers
*/
cliqueActiveSigners(blockNum) {
const signers = this._cliqueLatestSignerStates;
if (signers.length === 0) {
return [];
}
for (let i = signers.length - 1; i >= 0; i--) {
if (signers[i][0] < blockNum) {
return signers[i][1];
}
}
throw (0, util_1.EthereumJSErrorWithoutCode)(`Could not load signers for block ${blockNum}`);
}
/**
* 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
*/
cliqueSignerLimit(blockNum) {
return Math.floor(this.cliqueActiveSigners(blockNum).length / 2) + 1;
}
/**
* Checks if signer was recently signed.
* Returns true if signed too recently: more than once per {@link CliqueConsensus.cliqueSignerLimit} consecutive blocks.
* @param header BlockHeader
* @hidden
*/
cliqueCheckRecentlySigned(header) {
if (header.isGenesis() || header.number === BigInt(1)) {
// skip genesis, first block
return false;
}
const limit = this.cliqueSignerLimit(header.number);
// construct recent block signers list with this block
let signers = this._cliqueLatestBlockSigners;
signers = signers.slice(signers.length < limit ? 0 : 1);
if (signers.length > 0 && signers[signers.length - 1][0] !== header.number - BigInt(1)) {
// if the last signed block is not one minus the head we are trying to compare
// we do not have a complete picture of the state to verify if too recently signed
return false;
}
signers.push([header.number, (0, block_1.cliqueSigner)(header)]);
const seen = signers.filter((s) => s[1].equals((0, block_1.cliqueSigner)(header))).length;
return seen > 1;
}
/**
* Remove clique snapshots with blockNumber higher than input.
* @param blockNumber - the block number from which we start deleting
* @hidden
*/
async _cliqueDeleteSnapshots(blockNumber) {
// remove blockNumber from clique snapshots
// (latest signer states, latest votes, latest block signers)
this._cliqueLatestSignerStates = this._cliqueLatestSignerStates.filter((s) => s[0] <= blockNumber);
await this.cliqueUpdateSignerStates();
this._cliqueLatestVotes = this._cliqueLatestVotes.filter((v) => v[0] <= blockNumber);
await this.cliqueUpdateVotes();
this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.filter((s) => s[0] <= blockNumber);
await this.cliqueUpdateLatestBlockSigners();
}
/**
* Update snapshot of latest clique block signers.
* Used for checking for 'recently signed' error.
* Length trimmed to {@link Blockchain.cliqueSignerLimit}.
* @param header BlockHeader
* @hidden
*/
async cliqueUpdateLatestBlockSigners(header) {
if (header) {
if (header.isGenesis()) {
return;
}
// add this block's signer
const signer = [header.number, (0, block_1.cliqueSigner)(header)];
this._cliqueLatestBlockSigners.push(signer);
// trim length to `this.cliqueSignerLimit()`
const length = this._cliqueLatestBlockSigners.length;
const limit = this.cliqueSignerLimit(header.number);
if (length > limit) {
this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.slice(length - limit, length);
}
}
// save to db
const formatted = this._cliqueLatestBlockSigners.map((b) => [
(0, util_1.bigIntToBytes)(b[0]),
b[1].toBytes(),
]);
await this.blockchain.db.put(CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY, rlp_1.RLP.encode(formatted));
}
/**
* Fetches clique signers.
* @hidden
*/
async getCliqueLatestSignerStates() {
const signerStates = await this.blockchain.db.get(CLIQUE_SIGNERS_KEY);
if (signerStates === undefined)
return [];
const states = rlp_1.RLP.decode(signerStates);
return states.map((state) => {
const blockNum = (0, util_1.bytesToBigInt)(state[0]);
const addresses = state[1].map((bytes) => {
const address = new util_1.Address(bytes);
return address;
});
return [blockNum, addresses];
});
}
/**
* Fetches clique votes.
* @hidden
*/
async getCliqueLatestVotes() {
const signerVotes = await this.blockchain.db.get(CLIQUE_VOTES_KEY);
if (signerVotes === undefined)
return [];
const votes = rlp_1.RLP.decode(signerVotes);
return votes.map((vote) => {
const blockNum = (0, util_1.bytesToBigInt)(vote[0]);
const signer = new util_1.Address(vote[1][0]);
const beneficiary = new util_1.Address(vote[1][1]);
const nonce = vote[1][2];
return [blockNum, [signer, beneficiary, nonce]];
});
}
/**
* Fetches snapshot of clique signers.
* @hidden
*/
async getCliqueLatestBlockSigners() {
const blockSigners = await this.blockchain.db.get(CLIQUE_BLOCK_SIGNERS_SNAPSHOT_KEY);
if (blockSigners === undefined)
return [];
const signers = rlp_1.RLP.decode(blockSigners);
return signers.map((s) => {
const blockNum = (0, util_1.bytesToBigInt)(s[0]);
const signer = new util_1.Address(s[1]);
return [blockNum, signer];
});
}
/**
* Build clique snapshots.
* @param header - the new block header
* @hidden
*/
async _cliqueBuildSnapshots(header) {
if (!(0, block_1.cliqueIsEpochTransition)(header)) {
await this.cliqueUpdateVotes(header);
}
await this.cliqueUpdateLatestBlockSigners(header);
}
/**
* Helper to determine if a signer is in or out of turn for the next block.
* @param signer The signer address
*/
async cliqueSignerInTurn(signer, blockNum) {
const signers = this.cliqueActiveSigners(blockNum);
const signerIndex = signers.findIndex((address) => address.equals(signer));
if (signerIndex === -1) {
throw (0, util_1.EthereumJSErrorWithoutCode)('Signer not found');
}
const { number } = await this.blockchain.getCanonicalHeadHeader();
return (number + BigInt(1)) % BigInt(signers.length) === BigInt(signerIndex);
}
}
exports.CliqueConsensus = CliqueConsensus;
//# sourceMappingURL=clique.js.map