Skip to content

Commit

Permalink
clique: add reorg logic and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanio committed Jan 28, 2021
1 parent 6cbebb3 commit 8c81c69
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 29 deletions.
92 changes: 64 additions & 28 deletions packages/blockchain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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 = []

Expand All @@ -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 = []

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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)))

Expand All @@ -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
Expand All @@ -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]]
}

/**
Expand Down Expand Up @@ -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)
Expand Down
143 changes: 142 additions & 1 deletion packages/blockchain/test/reorg.spec.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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()
}
)
})

0 comments on commit 8c81c69

Please sign in to comment.