Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions packages/client/lib/blockchain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ export interface ChainBlocks {
*/
latest: Block | null

/**
* The block as signalled `finalized` in the fcU
* This corresponds to the last finalized beacon block
*/
finalized: Block | null

/**
* The block as signalled `safe` in the fcU
* This corresponds to the last justified beacon block
*/
safe: Block | null

/**
* The total difficulty of the blockchain
*/
Expand All @@ -56,6 +68,18 @@ export interface ChainHeaders {
*/
latest: BlockHeader | null

/**
* The block as signalled `finalized` in the fcU
* This corresponds to the last finalized beacon block
*/
finalized: BlockHeader | null

/**
* The block as signalled `safe` in the fcU
* This corresponds to the last justified beacon block
*/
safe: BlockHeader | null

/**
* The total difficulty of the headerchain
*/
Expand All @@ -79,12 +103,16 @@ export class Chain {

private _headers: ChainHeaders = {
latest: null,
finalized: null,
safe: null,
td: BigInt(0),
height: BigInt(0),
}

private _blocks: ChainBlocks = {
latest: null,
finalized: null,
safe: null,
td: BigInt(0),
height: BigInt(0),
}
Expand Down Expand Up @@ -136,11 +164,15 @@ export class Chain {
private reset() {
this._headers = {
latest: null,
finalized: null,
safe: null,
td: BigInt(0),
height: BigInt(0),
}
this._blocks = {
latest: null,
finalized: null,
safe: null,
td: BigInt(0),
height: BigInt(0),
}
Expand Down Expand Up @@ -221,17 +253,28 @@ export class Chain {

const headers: ChainHeaders = {
latest: null,
finalized: null,
safe: null,
td: BigInt(0),
height: BigInt(0),
}
const blocks: ChainBlocks = {
latest: null,
finalized: null,
safe: null,
td: BigInt(0),
height: BigInt(0),
}

headers.latest = await this.getCanonicalHeadHeader()
// finalized and safe are always blocks since they have to have valid execution
// before they can be saved in chain
headers.finalized = (await this.getCanonicalFinalizedBlock()).header
headers.safe = (await this.getCanonicalSafeBlock()).header

blocks.latest = await this.getCanonicalHeadBlock()
blocks.finalized = await this.getCanonicalFinalizedBlock()
blocks.safe = await this.getCanonicalSafeBlock()

headers.height = headers.latest.number
blocks.height = blocks.latest.header.number
Expand Down Expand Up @@ -316,16 +359,31 @@ export class Chain {
* @param fromEngine pass true to process post-merge blocks, otherwise they will be skipped
* @returns number of blocks added
*/
async putBlocks(blocks: Block[], fromEngine = false): Promise<number> {
async putBlocks(blocks: Block[], fromEngine = false, skipUpdateEmit = false): Promise<number> {
if (!this.opened) throw new Error('Chain closed')
if (blocks.length === 0) return 0

let numAdded = 0
for (const [i, b] of blocks.entries()) {
// filter out finalized blocks
const newBlocks = []
for (const block of blocks) {
if (this.headers.finalized !== null && block.header.number <= this.headers.finalized.number) {
const canonicalBlock = await this.getBlock(block.header.number)
if (!canonicalBlock.hash().equals(block.hash())) {
throw Error(
`Invalid putBlock for block=${block.header.number} before finalized=${this.headers.finalized.number}`
)
}
} else {
newBlocks.push(block)
}
}

for (const [i, b] of newBlocks.entries()) {
if (!fromEngine && this.config.chainCommon.gteHardfork(Hardfork.Merge)) {
if (i > 0) {
// emitOnLast below won't be reached, so run an update here
await this.update(true)
await this.update(!skipUpdateEmit)
}
break
}
Expand All @@ -343,8 +401,8 @@ export class Chain {

await this.blockchain.putBlock(block)
numAdded++
const emitOnLast = blocks.length === numAdded
await this.update(emitOnLast)
const emitOnLast = newBlocks.length === numAdded
await this.update(emitOnLast && !skipUpdateEmit)
}
return numAdded
}
Expand Down Expand Up @@ -414,6 +472,22 @@ export class Chain {
return this.blockchain.getCanonicalHeadBlock()
}

/**
* Gets the latest block in the canonical chain
*/
async getCanonicalSafeBlock(): Promise<Block> {
if (!this.opened) throw new Error('Chain closed')
return this.blockchain.getIteratorHead('safe')
}

/**
* Gets the latest block in the canonical chain
*/
async getCanonicalFinalizedBlock(): Promise<Block> {
if (!this.opened) throw new Error('Chain closed')
return this.blockchain.getIteratorHead('finalized')
}

/**
* Gets total difficulty for a block
* @param hash the block hash
Expand Down
61 changes: 53 additions & 8 deletions packages/client/lib/execution/vmexecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,27 +161,72 @@ export class VMExecution extends Execution {
* Should only be used after {@link VMExecution.runWithoutSetHead}
* @param blocks Array of blocks to save pending receipts and set the last block as the head
*/
async setHead(blocks: Block[]): Promise<void> {
async setHead(
blocks: Block[],
{ finalizedBlock, safeBlock }: { finalizedBlock?: Block; safeBlock?: Block } = {}
): Promise<void> {
return this.runWithLock<void>(async () => {
const vmHeadBlock = blocks[blocks.length - 1]
if (!(await this.vm.stateManager.hasStateRoot(vmHeadBlock.header.stateRoot))) {
// If we set blockchain iterator to somewhere where we don't have stateroot
// execution run will always fail
const chainPointers: [string, Block | null][] = [
['vmHeadBlock', vmHeadBlock],
// if safeBlock is not provided, the current safeBlock of chain should be used
// which is genesisBlock if it has never been set for e.g.
['safeBlock', safeBlock ?? this.chain.blocks.safe],
['finalizedBlock', finalizedBlock ?? this.chain.blocks.finalized],
]

let isSortedDesc = true
let lastBlock = vmHeadBlock
for (const [blockName, block] of chainPointers) {
if (block === null) {
continue
}
if (!(await this.vm.stateManager.hasStateRoot(block.header.stateRoot))) {
// If we set blockchain iterator to somewhere where we don't have stateroot
// execution run will always fail
throw Error(
`${blockName}'s stateRoot not found number=${block.header.number} root=${short(
block.header.stateRoot
)}`
)
}
isSortedDesc = isSortedDesc && lastBlock.header.number >= block.header.number
lastBlock = block
}

if (isSortedDesc === false) {
throw Error(
`vmHeadBlock's stateRoot not found number=${vmHeadBlock.header.number} root=${short(
vmHeadBlock.header.stateRoot
)}`
`headBlock=${vmHeadBlock?.header.number} should be >= safeBlock=${safeBlock?.header.number} should be >= finalizedBlock=${finalizedBlock?.header.number}`
)
}
await this.chain.putBlocks(blocks, true)
// skip emitting the chain update event as we will manually do it
await this.chain.putBlocks(blocks, true, true)
for (const block of blocks) {
const receipts = this.pendingReceipts?.get(block.hash().toString('hex'))
if (receipts) {
void this.receiptsManager?.saveReceipts(block, receipts)
this.pendingReceipts?.delete(block.hash().toString('hex'))
}
}

// check if the head, safe and finalized are now canonical
for (const [blockName, block] of chainPointers) {
if (block === null) {
continue
}
const blockByNumber = await this.chain.getBlock(block.header.number)
if (!blockByNumber.hash().equals(block.hash())) {
Copy link
Contributor Author

@g11tech g11tech Mar 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canonicalness of head, safe and finalized is now checked here

throw Error(`${blockName} not in canonical chain`)
}
}
await this.chain.blockchain.setIteratorHead('vm', vmHeadBlock.hash())
if (safeBlock !== undefined) {
await this.chain.blockchain.setIteratorHead('safe', safeBlock.hash())
}
if (finalizedBlock !== undefined) {
await this.chain.blockchain.setIteratorHead('finalized', finalizedBlock.hash())
}
await this.chain.update(true)
})
}

Expand Down
100 changes: 60 additions & 40 deletions packages/client/lib/rpc/modules/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type { FullEthereumService } from '../../service'
import type { HeaderData } from '@ethereumjs/block'
import type { VM } from '@ethereumjs/vm'

const zeroBlockHash = zeros(32)

export enum Status {
ACCEPTED = 'ACCEPTED',
INVALID = 'INVALID',
Expand Down Expand Up @@ -721,6 +723,16 @@ export class Engine {
const { headBlockHash, finalizedBlockHash, safeBlockHash } = params[0]
const payloadAttributes = params[1]

const safe = toBuffer(safeBlockHash)
const finalized = toBuffer(finalizedBlockHash)

if (!finalized.equals(zeroBlockHash) && safe.equals(zeroBlockHash)) {
throw {
code: INVALID_PARAMS,
message: 'safe block can not be zero if finalized is not zero',
}
}

if (this.config.synchronized) {
this.connectionManager.newForkchoiceLog()
}
Expand Down Expand Up @@ -804,6 +816,46 @@ export class Engine {
return response
}

/*
* Process safe and finalized block since headBlock has been found to be executed
* Allowed to have zero value while transition block is finalizing
*/
let safeBlock, finalizedBlock

if (!safe.equals(zeroBlockHash)) {
if (safe.equals(headBlock.hash())) {
safeBlock = headBlock
} else {
try {
// Right now only check if the block is available, canonicality check is done
// in setHead after chain.putBlocks so as to reflect latest canonical chain
safeBlock = await this.chain.getBlock(safe)
} catch (_error: any) {
throw {
code: INVALID_PARAMS,
message: 'safe block not available',
}
}
}
} else {
safeBlock = undefined
}

if (!finalized.equals(zeroBlockHash)) {
try {
// Right now only check if the block is available, canonicality check is done
// in setHead after chain.putBlocks so as to reflect latest canonical chain
finalizedBlock = await this.chain.getBlock(finalized)
} catch (error: any) {
throw {
message: 'finalized block not available',
code: INVALID_PARAMS,
}
}
} else {
finalizedBlock = undefined
}

const vmHeadHash = this.chain.headers.latest!.hash()
if (!vmHeadHash.equals(headBlock.hash())) {
let parentBlocks: Block[] = []
Expand All @@ -826,7 +878,14 @@ export class Engine {
}

const blocks = [...parentBlocks, headBlock]
await this.execution.setHead(blocks)
try {
await this.execution.setHead(blocks, { safeBlock, finalizedBlock })
} catch (error) {
throw {
message: (error as Error).message,
code: INVALID_PARAMS,
}
}
this.service.txPool.removeNewBlockTxs(blocks)

const isPrevSynced = this.chain.config.synchronized
Expand All @@ -835,45 +894,6 @@ export class Engine {
this.service.txPool.checkRunState()
}
}
/*
* Process safe and finalized block
* Allowed to have zero value while transition block is finalizing
*/
const zeroBlockHash = zeros(32)
const safe = toBuffer(safeBlockHash)
if (!safe.equals(headBlock.hash()) && !safe.equals(zeroBlockHash)) {
const msg = 'Safe block not in canonical chain'
try {
const safeBlock = await this.chain.getBlock(safe)
const canonical = await this.chain.getBlock(safeBlock.header.number)
if (!canonical.hash().equals(safe)) {
throw new Error(msg)
}
} catch (error: any) {
const message = error.message === msg ? msg : 'safe block not available'
throw {
code: INVALID_PARAMS,
message,
}
}
}
const finalized = toBuffer(finalizedBlockHash)
if (!finalized.equals(zeroBlockHash)) {
const msg = 'Finalized block not in canonical chain'
try {
const finalizedBlock = await this.chain.getBlock(finalized)
const canonical = await this.chain.getBlock(finalizedBlock.header.number)
if (!canonical.hash().equals(finalized)) {
throw new Error(msg)
}
} catch (error: any) {
const message = error.message === msg ? msg : 'finalized block not available'
throw {
message,
code: INVALID_PARAMS,
}
}
}

/*
* If payloadAttributes is present, start building block and return payloadId
Expand Down
Loading