Skip to content

Commit

Permalink
Clique: get signers by block num (#2610)
Browse files Browse the repository at this point in the history
* blockchain/clique: add support for clique signers by block num

* blockchain: clique sort clique latest signer states

* client: fix build

* blockchain/clique: fix tests and add test

* blockchain/clique: fix clique bugs

* blockchain/tests/clique: fix tests

* blockchain: clique: add reorg test

* blockchain/clique: add reorg over epoch test (fails) [no ci]

* replace equalsBytes with deepEquals

* blockchain/clique: address comments

* blockchain/clique: ensure tests are ran

---------

Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com>
  • Loading branch information
jochem-brouwer and acolytec3 authored Apr 10, 2023
1 parent fd2e4ea commit a42c10e
Show file tree
Hide file tree
Showing 3 changed files with 509 additions and 235 deletions.
61 changes: 43 additions & 18 deletions packages/blockchain/src/consensus/clique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ type CliqueLatestBlockSigners = CliqueBlockSigner[]

/**
* This class encapsulates Clique-related consensus functionality when used with the Blockchain class.
* Note: reorgs which happen between epoch transitions, which change the internal voting state over the reorg
* will result in failure and is currently not supported.
* The hotfix for this could be: re-load the latest epoch block (this has the clique state in the extraData of the header)
* Now replay all blocks on top of it. This should validate the chain up to the new/reorged tip which previously threw.
*/
export class CliqueConsensus implements Consensus {
blockchain: Blockchain | undefined
Expand All @@ -58,7 +62,7 @@ export class CliqueConsensus implements Consensus {
*
* This defines a limit for reorgs on PoA clique chains.
*/
private CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 100
private CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT = 200

/**
* List with the latest signer states checkpointed on blocks where
Expand Down Expand Up @@ -112,6 +116,7 @@ export class CliqueConsensus implements Consensus {
async setup({ blockchain }: ConsensusOptions): Promise<void> {
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()
}
Expand All @@ -126,7 +131,7 @@ export class CliqueConsensus implements Consensus {
}

const { header } = block
const valid = header.cliqueVerifySignature(this.cliqueActiveSigners())
const valid = header.cliqueVerifySignature(this.cliqueActiveSigners(header.number))
if (!valid) {
throw new Error('invalid PoA block signature (clique)')
}
Expand All @@ -140,7 +145,7 @@ export class CliqueConsensus implements Consensus {
// only active (non-stale) votes will counted (if vote.blockNumber >= lastEpochBlockNumber

const checkpointSigners = header.cliqueEpochTransitionSigners()
const activeSigners = this.cliqueActiveSigners()
const activeSigners = this.cliqueActiveSigners(header.number)
for (const [i, cSigner] of checkpointSigners.entries()) {
if (activeSigners[i]?.equals(cSigner) !== true) {
throw new Error(
Expand All @@ -161,7 +166,7 @@ export class CliqueConsensus implements Consensus {
throw new Error(`${msg} ${header.errorStr()}`)
}

const signers = this.cliqueActiveSigners()
const signers = this.cliqueActiveSigners(header.number)
if (signers.length === 0) {
// abort if signers are unavailable
const msg = 'no signers available'
Expand Down Expand Up @@ -215,7 +220,17 @@ export class CliqueConsensus implements Consensus {
*/
private async cliqueUpdateSignerStates(signerState?: CliqueSignerState) {
if (signerState) {
const blockNumber = signerState[0]
const known = this._cliqueLatestSignerStates.find((value) => {
if (value[0] === blockNumber) {
return true
}
})
if (known !== undefined) {
return
}
this._cliqueLatestSignerStates.push(signerState)
this._cliqueLatestSignerStates.sort((a, b) => (a[0] > b[0] ? 1 : -1))
}

// trim to CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
Expand All @@ -240,10 +255,15 @@ export class CliqueConsensus implements Consensus {
])
await this.blockchain!.db.put(CLIQUE_SIGNERS_KEY, RLP.encode(formatted), DB_OPTS)
// Output active signers for debugging purposes
let i = 0
for (const signer of this.cliqueActiveSigners()) {
debug(`Clique signer [${i}]: ${signer}`)
i++
if (signerState !== undefined) {
let i = 0
try {
for (const signer of this.cliqueActiveSigners(signerState[0])) {
debug(`Clique signer [${i}]: ${signer} (block: ${signerState[0]})`)
i++
}
// eslint-disable-next-line no-empty
} catch (e) {}
}
}

Expand All @@ -268,8 +288,8 @@ export class CliqueConsensus implements Consensus {
header.number -
(header.number %
BigInt((this.blockchain!._common.consensusConfig() as CliqueConfig).epoch))
const limit = this.cliqueSignerLimit()
let activeSigners = this.cliqueActiveSigners()
const limit = this.cliqueSignerLimit(header.number)
let activeSigners = [...this.cliqueActiveSigners(header.number)]
let consensus = false

// AUTH vote analysis
Expand Down Expand Up @@ -400,12 +420,17 @@ export class CliqueConsensus implements Consensus {
/**
* Returns a list with the current block signers
*/
cliqueActiveSigners(): Address[] {
cliqueActiveSigners(blockNum: bigint): Address[] {
const signers = this._cliqueLatestSignerStates
if (signers.length === 0) {
return []
}
return [...signers[signers.length - 1][1]]
for (let i = signers.length - 1; i >= 0; i--) {
if (signers[i][0] < blockNum) {
return signers[i][1]
}
}
throw new Error(`Could not load signers for block ${blockNum}`)
}

/**
Expand All @@ -415,8 +440,8 @@ export class CliqueConsensus implements Consensus {
* 1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 3, ...
* @hidden
*/
private cliqueSignerLimit() {
return Math.floor(this.cliqueActiveSigners().length / 2) + 1
private cliqueSignerLimit(blockNum: bigint) {
return Math.floor(this.cliqueActiveSigners(blockNum).length / 2) + 1
}

/**
Expand All @@ -430,7 +455,7 @@ export class CliqueConsensus implements Consensus {
// skip genesis, first block
return false
}
const limit = this.cliqueSignerLimit()
const limit = this.cliqueSignerLimit(header.number)
// construct recent block signers list with this block
let signers = this._cliqueLatestBlockSigners
signers = signers.slice(signers.length < limit ? 0 : 1)
Expand Down Expand Up @@ -484,7 +509,7 @@ export class CliqueConsensus implements Consensus {

// trim length to `this.cliqueSignerLimit()`
const length = this._cliqueLatestBlockSigners.length
const limit = this.cliqueSignerLimit()
const limit = this.cliqueSignerLimit(header.number)
if (length > limit) {
this._cliqueLatestBlockSigners = this._cliqueLatestBlockSigners.slice(
length - limit,
Expand Down Expand Up @@ -591,8 +616,8 @@ export class CliqueConsensus implements Consensus {
* Helper to determine if a signer is in or out of turn for the next block.
* @param signer The signer address
*/
async cliqueSignerInTurn(signer: Address): Promise<boolean> {
const signers = this.cliqueActiveSigners()
async cliqueSignerInTurn(signer: Address, blockNum: bigint): Promise<boolean> {
const signers = this.cliqueActiveSigners(blockNum)
const signerIndex = signers.findIndex((address) => address.equals(signer))
if (signerIndex === -1) {
throw new Error('Signer not found')
Expand Down
Loading

0 comments on commit a42c10e

Please sign in to comment.