-
Notifications
You must be signed in to change notification settings - Fork 773
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
Implement debug_setHead #3811
Changes from 7 commits
cf4cff9
d66bfbd
9903f82
9b54275
a8884ea
c6918c0
c4e7294
62e2b13
224df36
e0aa82e
36a3d11
7e2bfaa
bdd8edd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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], | ||
]) | ||
|
@@ -457,4 +460,37 @@ | |
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. | ||
* @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`, | ||
} | ||
} | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated it to set the execution head as well. 🙂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, can you also add this check in the tests? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated the tests to include it. |
||
await this.service.execution.runWithoutSetHead({ block }) | ||
await this.service.execution.setHead([block]) | ||
} catch (e) { | ||
throw { | ||
code: INTERNAL_ERROR, | ||
} | ||
} | ||
headHash = await this.service.skeleton?.headHash() | ||
const newHead = headHash ? bytesToHex(headHash!) : undefined | ||
|
||
return `oldHead: ${oldHead} - newHead: ${newHead}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
} |
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) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { Block, createBlock, createBlockHeader } from '@ethereumjs/block' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I copied these helpers from the blockchain package test helpers in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.