Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement debug_setHead #3811

Merged
merged 13 commits into from
Dec 16, 2024
Merged
34 changes: 34 additions & 0 deletions packages/client/src/rpc/modules/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@
1,
[[validators.hex]],
)
this.setHead = middleware(callWithStackTrace(this.setHead.bind(this), this._rpcDebug), 1, [
[validators.blockOption],
])
this.verbosity = middleware(callWithStackTrace(this.verbosity.bind(this), this._rpcDebug), 1, [
[validators.unsignedInteger],
])
Expand Down Expand Up @@ -457,4 +460,35 @@
this.client.config.logger.configure({ level: logLevels[level] })
return `level: ${this.client.config.logger.level}`
}

/**
* Sets the current head of the local chain by block number. Note, this is a
* destructive action and may severely damage your chain. Use with extreme
* caution.
Copy link
Member

Choose a reason for hiding this comment

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

This looks like the docs from geth, are these also applicable here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it is implemented with force=true, I'd say it can be destructive, so it would be good to know in the docs.

* @param blockOpt Block number or tag to set as head of chain
*/
async setHead(params: [string]) {
const [blockOpt] = params
if (blockOpt === 'pending') {
throw {
code: INVALID_PARAMS,
message: `"pending" is not supported`,
}
}

Check warning on line 477 in packages/client/src/rpc/modules/debug.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/src/rpc/modules/debug.ts#L473-L477

Added lines #L473 - L477 were not covered by tests

let headHash = await this.service.skeleton?.headHash()
const oldHead = headHash ? bytesToHex(headHash!) : undefined
const block = await getBlockByOption(blockOpt, this.chain)
try {
await this.service.skeleton?.setHead(block, true)
Copy link
Member

Choose a reason for hiding this comment

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

This sets the skeleton head. Should the vm execution part also not be updated? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've updated it to set the execution head as well. 🙂

Copy link
Member

Choose a reason for hiding this comment

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

Cool, can you also add this check in the tests? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've updated the tests to include it.

} catch {
throw {
code: INTERNAL_ERROR,
}
}

Check warning on line 488 in packages/client/src/rpc/modules/debug.ts

View check run for this annotation

Codecov / codecov/patch

packages/client/src/rpc/modules/debug.ts#L485-L488

Added lines #L485 - L488 were not covered by tests
headHash = await this.service.skeleton?.headHash()
const newHead = headHash ? bytesToHex(headHash!) : undefined

return `oldHead: ${oldHead} - newHead: ${newHead}`
Copy link
Contributor

Choose a reason for hiding this comment

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

One thought I had here is it would be worth making our response value match Geth's as well. Strings like above are pretty non-standard for RPC responses and you can't parse them programmatically without having to do fancy string parsing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, I think you're right will remove. It was a convenient way to develop, but will change it to match.

}
}
41 changes: 41 additions & 0 deletions packages/client/test/rpc/debug/setHead.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { bigIntToHex } from '@ethereumjs/util'
import { assert, describe, it } from 'vitest'

import { createClient, createManager, getRPCClient, startRPC } from '../helpers.js'

import { generateBlockchain, generateConsecutiveBlock } from './util.js'

const method = 'debug_setHead'

describe(method, async () => {
it('call with valid arguments', async () => {
const { blockchain, blocks, _ } = await generateBlockchain(3)
const a = await createClient({ blockchain })
await a.service.skeleton?.open()
const manager = createManager(a)
const rpc = getRPCClient(startRPC(manager.getMethods()))

assert.equal(
await a.service.skeleton?.headHash(),
undefined,
'should return undefined when head is not set',
)
for (let i = 0; i < blocks.length; i++) {
await rpc.request(method, [`0x${i}`])
assert.deepEqual(
await a.service.skeleton?.headHash()!,
await blocks[i].header.hash(),
`should return hash of block number ${i} set as head`,
)
}

const newCanonicalHead = generateConsecutiveBlock(blocks[2], 1)
await blockchain.putBlocks([newCanonicalHead])
await rpc.request(method, [bigIntToHex(newCanonicalHead.header.number)])
assert.deepEqual(
await a.service.skeleton?.headHash()!,
newCanonicalHead.header.hash(),
`should set hash to new head when there is a fork`,
)
}, 30000)
})
96 changes: 96 additions & 0 deletions packages/client/test/rpc/debug/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Block, createBlock, createBlockHeader } from '@ethereumjs/block'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I copied these helpers from the blockchain package test helpers in packages/blockchain/test/util.ts. Any thoughts on extracting these out to the util package so that they can be imported for testing and maybe production use cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While looking more into this, I realized this is not possible since it would introduce circular dependencies.

import { createBlockchain } from '@ethereumjs/blockchain'
import { Common, Hardfork, Mainnet } from '@ethereumjs/common'

export const generateBlocks = (numberOfBlocks: number, existingBlocks?: Block[]): Block[] => {
const blocks = existingBlocks ? existingBlocks : []

const gasLimit = 8000000
const common = new Common({ chain: Mainnet, hardfork: Hardfork.Chainstart })
const opts = { common }

if (blocks.length === 0) {
const genesis = createBlock({ header: { gasLimit } }, opts)
blocks.push(genesis)
}

for (let i = blocks.length; i < numberOfBlocks; i++) {
const lastBlock = blocks[i - 1]
const blockData = {
header: {
number: i,
parentHash: lastBlock.hash(),
gasLimit,
timestamp: lastBlock.header.timestamp + BigInt(1),
},
}
const block = createBlock(blockData, {
common,
calcDifficultyFromHeader: lastBlock.header,
})
blocks.push(block)
}

return blocks
}

export const generateBlockchain = async (numberOfBlocks: number, genesis?: Block): Promise<any> => {
const existingBlocks: Block[] = genesis ? [genesis] : []
const blocks = generateBlocks(numberOfBlocks, existingBlocks)

const blockchain = await createBlockchain({
validateBlocks: true,
genesisBlock: genesis ?? blocks[0],
})
try {
await blockchain.putBlocks(blocks.slice(1))
} catch (error: any) {
return { error }
}

return {
blockchain,
blocks,
error: null,
}
}
/**
*
* @param parentBlock parent block to generate the consecutive block on top of
* @param difficultyChangeFactor this integer can be any value, but will only return unique blocks between [-99, 1] (this is due to difficulty calculation). 1 will increase the difficulty, 0 will keep the difficulty constant any any negative number will decrease the difficulty
*/

export const generateConsecutiveBlock = (
parentBlock: Block,
difficultyChangeFactor: number,
gasLimit: bigint = BigInt(8000000),
): Block => {
if (difficultyChangeFactor > 1) {
difficultyChangeFactor = 1
}
const common = new Common({ chain: Mainnet, hardfork: Hardfork.MuirGlacier })
const tmpHeader = createBlockHeader(
{
number: parentBlock.header.number + BigInt(1),
timestamp: parentBlock.header.timestamp + BigInt(10 + -difficultyChangeFactor * 9),
},
{ common },
)
const header = createBlockHeader(
{
number: parentBlock.header.number + BigInt(1),
parentHash: parentBlock.hash(),
gasLimit,
timestamp: parentBlock.header.timestamp + BigInt(10 + -difficultyChangeFactor * 9),
difficulty: tmpHeader.ethashCanonicalDifficulty(parentBlock.header),
},
{
common,
calcDifficultyFromHeader: parentBlock.header,
},
)

const block = new Block(header, undefined, undefined, undefined, { common }, undefined)

return block
}
Loading