diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 25848fa589..1c3cb22faf 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -507,15 +507,6 @@ export class Block { } } - /** - * Returns the canonical difficulty for this block. - * - * @param parentBlock - the parent of this `Block` - */ - ethashCanonicalDifficulty(parentBlock: Block): bigint { - return this.header.ethashCanonicalDifficulty(parentBlock.header) - } - /** * Validates if the block gasLimit remains in the boundaries set by the protocol. * Throws if invalid diff --git a/packages/block/src/clique.ts b/packages/block/src/clique.ts deleted file mode 100644 index 16f1edfcfc..0000000000 --- a/packages/block/src/clique.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Fixed number of extra-data prefix bytes reserved for signer vanity -export const CLIQUE_EXTRA_VANITY = 32 -// Fixed number of extra-data suffix bytes reserved for signer seal -export const CLIQUE_EXTRA_SEAL = 65 diff --git a/packages/block/src/consensus/clique.ts b/packages/block/src/consensus/clique.ts new file mode 100644 index 0000000000..3a89a46319 --- /dev/null +++ b/packages/block/src/consensus/clique.ts @@ -0,0 +1,163 @@ +import { ConsensusAlgorithm } from '@ethereumjs/common' +import { RLP } from '@ethereumjs/rlp' +import { + Address, + BIGINT_0, + BIGINT_27, + bigIntToBytes, + bytesToBigInt, + concatBytes, + createAddressFromPublicKey, + createZeroAddress, + ecrecover, + ecsign, + equalsBytes, +} from '@ethereumjs/util' + +import type { BlockHeader } from '../index.js' +import type { CliqueConfig } from '@ethereumjs/common' + +// Fixed number of extra-data prefix bytes reserved for signer vanity +export const CLIQUE_EXTRA_VANITY = 32 +// Fixed number of extra-data suffix bytes reserved for signer seal +export const CLIQUE_EXTRA_SEAL = 65 + +// This function is not exported in the index file to keep it internal +export function requireClique(header: BlockHeader, name: string) { + if (header.common.consensusAlgorithm() !== ConsensusAlgorithm.Clique) { + const msg = header['_errorMsg']( + `BlockHeader.${name}() call only supported for clique PoA networks`, + ) + throw new Error(msg) + } +} + +/** + * PoA clique signature hash without the seal. + */ +export function cliqueSigHash(header: BlockHeader) { + requireClique(header, 'cliqueSigHash') + const raw = header.raw() + raw[12] = header.extraData.subarray(0, header.extraData.length - CLIQUE_EXTRA_SEAL) + return header['keccakFunction'](RLP.encode(raw)) +} + +/** + * Checks if the block header is an epoch transition + * header (only clique PoA, throws otherwise) + */ +export function cliqueIsEpochTransition(header: BlockHeader): boolean { + requireClique(header, 'cliqueIsEpochTransition') + const epoch = BigInt((header.common.consensusConfig() as CliqueConfig).epoch) + // Epoch transition block if the block number has no + // remainder on the division by the epoch length + return header.number % epoch === BIGINT_0 +} + +/** + * Returns extra vanity data + * (only clique PoA, throws otherwise) + */ +export function cliqueExtraVanity(header: BlockHeader): Uint8Array { + requireClique(header, 'cliqueExtraVanity') + return header.extraData.subarray(0, CLIQUE_EXTRA_VANITY) +} + +/** + * Returns extra seal data + * (only clique PoA, throws otherwise) + */ +export function cliqueExtraSeal(header: BlockHeader): Uint8Array { + requireClique(header, 'cliqueExtraSeal') + return header.extraData.subarray(-CLIQUE_EXTRA_SEAL) +} + +/** + * Returns a list of signers + * (only clique PoA, throws otherwise) + * + * This function throws if not called on an epoch + * transition block and should therefore be used + * in conjunction with {@link BlockHeader.cliqueIsEpochTransition} + */ +export function cliqueEpochTransitionSigners(header: BlockHeader): Address[] { + requireClique(header, 'cliqueEpochTransitionSigners') + if (!cliqueIsEpochTransition(header)) { + const msg = header['_errorMsg']('Signers are only included in epoch transition blocks (clique)') + throw new Error(msg) + } + + const start = CLIQUE_EXTRA_VANITY + const end = header.extraData.length - CLIQUE_EXTRA_SEAL + const signerBytes = header.extraData.subarray(start, end) + + const signerList: Uint8Array[] = [] + const signerLength = 20 + for (let start = 0; start <= signerBytes.length - signerLength; start += signerLength) { + signerList.push(signerBytes.subarray(start, start + signerLength)) + } + return signerList.map((buf) => new Address(buf)) +} + +/** + * Returns the signer address + */ +export function cliqueSigner(header: BlockHeader): Address { + requireClique(header, 'cliqueSigner') + const extraSeal = cliqueExtraSeal(header) + // Reasonable default for default blocks + if (extraSeal.length === 0 || equalsBytes(extraSeal, new Uint8Array(65))) { + return createZeroAddress() + } + const r = extraSeal.subarray(0, 32) + const s = extraSeal.subarray(32, 64) + const v = bytesToBigInt(extraSeal.subarray(64, 65)) + BIGINT_27 + const pubKey = ecrecover(cliqueSigHash(header), v, r, s) + return createAddressFromPublicKey(pubKey) +} + +/** + * Verifies the signature of the block (last 65 bytes of extraData field) + * (only clique PoA, throws otherwise) + * + * Method throws if signature is invalid + */ +export function cliqueVerifySignature(header: BlockHeader, signerList: Address[]): boolean { + requireClique(header, 'cliqueVerifySignature') + const signerAddress = cliqueSigner(header) + const signerFound = signerList.find((signer) => { + return signer.equals(signerAddress) + }) + return !!signerFound +} + +/** + * Generates the extraData from a sealed block header + * @param header block header from which to retrieve extraData + * @param cliqueSigner clique signer key used for creating sealed block + * @returns clique seal (i.e. extradata) for the block + */ +export function generateCliqueBlockExtraData( + header: BlockHeader, + cliqueSigner: Uint8Array, +): Uint8Array { + // Ensure extraData is at least length CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL + const minExtraDataLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL + if (header.extraData.length < minExtraDataLength) { + const remainingLength = minExtraDataLength - header.extraData.length + ;(header.extraData as any) = concatBytes(header.extraData, new Uint8Array(remainingLength)) + } + + requireClique(header, 'generateCliqueBlockExtraData') + + const ecSignFunction = header.common.customCrypto?.ecsign ?? ecsign + const signature = ecSignFunction(cliqueSigHash(header), cliqueSigner) + const signatureB = concatBytes(signature.r, signature.s, bigIntToBytes(signature.v - BIGINT_27)) + + const extraDataWithoutSeal = header.extraData.subarray( + 0, + header.extraData.length - CLIQUE_EXTRA_SEAL, + ) + const extraData = concatBytes(extraDataWithoutSeal, signatureB) + return extraData +} diff --git a/packages/block/src/consensus/ethash.ts b/packages/block/src/consensus/ethash.ts new file mode 100644 index 0000000000..fec0b0378a --- /dev/null +++ b/packages/block/src/consensus/ethash.ts @@ -0,0 +1,10 @@ +import type { Block } from '../index.js' + +/** + * Returns the canonical difficulty for this block. + * + * @param parentBlock - the parent of this `Block` + */ +export function ethashCanonicalDifficulty(block: Block, parentBlock: Block): bigint { + return block.header.ethashCanonicalDifficulty(parentBlock.header) +} diff --git a/packages/block/src/consensus/index.ts b/packages/block/src/consensus/index.ts new file mode 100644 index 0000000000..dd60522000 --- /dev/null +++ b/packages/block/src/consensus/index.ts @@ -0,0 +1,12 @@ +export { + CLIQUE_EXTRA_SEAL, + CLIQUE_EXTRA_VANITY, + cliqueEpochTransitionSigners, + cliqueExtraSeal, + cliqueExtraVanity, + cliqueIsEpochTransition, + cliqueSigHash, + cliqueSigner, + cliqueVerifySignature, +} from './clique.js' +export * from './ethash.js' diff --git a/packages/block/src/constructors.ts b/packages/block/src/constructors.ts index c6a413cb6e..4f346e1308 100644 --- a/packages/block/src/constructors.ts +++ b/packages/block/src/constructors.ts @@ -24,6 +24,7 @@ import { isHexString, } from '@ethereumjs/util' +import { generateCliqueBlockExtraData } from './consensus/clique.js' import { createBlockFromRpc } from './from-rpc.js' import { genRequestsTrieRoot, @@ -515,3 +516,46 @@ export async function createBlockFromBeaconPayloadJson( const executionPayload = executionPayloadFromBeaconPayload(payload) return createBlockFromExecutionPayload(executionPayload, opts) } + +export function createSealedCliqueBlock( + blockData: BlockData = {}, + cliqueSigner: Uint8Array, + opts: BlockOptions = {}, +): Block { + const sealedCliqueBlock = createBlock(blockData, { + ...opts, + ...{ freeze: false, skipConsensusFormatValidation: true }, + }) + ;(sealedCliqueBlock.header.extraData as any) = generateCliqueBlockExtraData( + sealedCliqueBlock.header, + cliqueSigner, + ) + if (opts?.freeze === true) { + // We have to freeze here since we can't freeze the block when constructing it since we are overwriting `extraData` + Object.freeze(sealedCliqueBlock) + } + if (opts?.skipConsensusFormatValidation === false) { + // We need to validate the consensus format here since we skipped it when constructing the block + sealedCliqueBlock.header['_consensusFormatValidation']() + } + return sealedCliqueBlock +} + +export function createSealedCliqueBlockHeader( + headerData: HeaderData = {}, + cliqueSigner: Uint8Array, + opts: BlockOptions = {}, +): BlockHeader { + const sealedCliqueBlockHeader = new BlockHeader(headerData, { + ...opts, + ...{ skipConsensusFormatValidation: true }, + }) + ;(sealedCliqueBlockHeader.extraData as any) = generateCliqueBlockExtraData( + sealedCliqueBlockHeader, + cliqueSigner, + ) + if (opts.skipConsensusFormatValidation === false) + // We need to validate the consensus format here since we skipped it when constructing the block header + sealedCliqueBlockHeader['_consensusFormatValidation']() + return sealedCliqueBlockHeader +} diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 88830ab200..eed3e04c24 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -5,22 +5,15 @@ import { BIGINT_0, BIGINT_1, BIGINT_2, - BIGINT_27, BIGINT_7, KECCAK256_RLP, KECCAK256_RLP_ARRAY, TypeOutput, - bigIntToBytes, bigIntToHex, bigIntToUnpaddedBytes, - bytesToBigInt, bytesToHex, bytesToUtf8, - concatBytes, - createAddressFromPublicKey, createZeroAddress, - ecrecover, - ecsign, equalsBytes, hexToBytes, toType, @@ -28,12 +21,15 @@ import { } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' -import { CLIQUE_EXTRA_SEAL, CLIQUE_EXTRA_VANITY } from './clique.js' +import { + CLIQUE_EXTRA_SEAL, + CLIQUE_EXTRA_VANITY, + cliqueIsEpochTransition, +} from './consensus/clique.js' import { fakeExponential } from './helpers.js' import { paramsBlock } from './params.js' import type { BlockHeaderBytes, BlockOptions, HeaderData, JsonHeader } from './types.js' -import type { CliqueConfig } from '@ethereumjs/common' interface HeaderCache { hash: Uint8Array | undefined @@ -247,18 +243,6 @@ export class BlockHeader { this.difficulty = this.ethashCanonicalDifficulty(opts.calcDifficultyFromHeader) } - // If cliqueSigner is provided, seal block with provided privateKey. - if (opts.cliqueSigner) { - // Ensure extraData is at least length CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL - const minExtraDataLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL - if (this.extraData.length < minExtraDataLength) { - const remainingLength = minExtraDataLength - this.extraData.length - this.extraData = concatBytes(this.extraData, new Uint8Array(remainingLength)) - } - - this.extraData = this.cliqueSealBlock(opts.cliqueSigner) - } - // Validate consensus format after block is sealed (if applicable) so extraData checks will pass if (skipValidateConsensusFormat === false) this._consensusFormatValidation() @@ -387,7 +371,7 @@ export class BlockHeader { if (this.common.consensusAlgorithm() === ConsensusAlgorithm.Clique) { // PoA/Clique const minLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL - if (!this.cliqueIsEpochTransition()) { + if (!cliqueIsEpochTransition(this)) { // ExtraData length on epoch transition if (this.extraData.length !== minLength) { const msg = this._errorMsg( @@ -669,15 +653,6 @@ export class BlockHeader { return this.number === BIGINT_0 } - protected _requireClique(name: string) { - if (this.common.consensusAlgorithm() !== ConsensusAlgorithm.Clique) { - const msg = this._errorMsg( - `BlockHeader.${name}() call only supported for clique PoA networks`, - ) - throw new Error(msg) - } - } - /** * Returns the canonical difficulty for this block. * @@ -751,125 +726,6 @@ export class BlockHeader { return dif } - /** - * PoA clique signature hash without the seal. - */ - cliqueSigHash() { - this._requireClique('cliqueSigHash') - const raw = this.raw() - raw[12] = this.extraData.subarray(0, this.extraData.length - CLIQUE_EXTRA_SEAL) - return this.keccakFunction(RLP.encode(raw)) - } - - /** - * Checks if the block header is an epoch transition - * header (only clique PoA, throws otherwise) - */ - cliqueIsEpochTransition(): boolean { - this._requireClique('cliqueIsEpochTransition') - const epoch = BigInt((this.common.consensusConfig() as CliqueConfig).epoch) - // Epoch transition block if the block number has no - // remainder on the division by the epoch length - return this.number % epoch === BIGINT_0 - } - - /** - * Returns extra vanity data - * (only clique PoA, throws otherwise) - */ - cliqueExtraVanity(): Uint8Array { - this._requireClique('cliqueExtraVanity') - return this.extraData.subarray(0, CLIQUE_EXTRA_VANITY) - } - - /** - * Returns extra seal data - * (only clique PoA, throws otherwise) - */ - cliqueExtraSeal(): Uint8Array { - this._requireClique('cliqueExtraSeal') - return this.extraData.subarray(-CLIQUE_EXTRA_SEAL) - } - - /** - * Seal block with the provided signer. - * Returns the final extraData field to be assigned to `this.extraData`. - * @hidden - */ - private cliqueSealBlock(privateKey: Uint8Array) { - this._requireClique('cliqueSealBlock') - - const ecSignFunction = this.common.customCrypto?.ecsign ?? ecsign - const signature = ecSignFunction(this.cliqueSigHash(), privateKey) - const signatureB = concatBytes(signature.r, signature.s, bigIntToBytes(signature.v - BIGINT_27)) - - const extraDataWithoutSeal = this.extraData.subarray( - 0, - this.extraData.length - CLIQUE_EXTRA_SEAL, - ) - const extraData = concatBytes(extraDataWithoutSeal, signatureB) - return extraData - } - - /** - * Returns a list of signers - * (only clique PoA, throws otherwise) - * - * This function throws if not called on an epoch - * transition block and should therefore be used - * in conjunction with {@link BlockHeader.cliqueIsEpochTransition} - */ - cliqueEpochTransitionSigners(): Address[] { - this._requireClique('cliqueEpochTransitionSigners') - if (!this.cliqueIsEpochTransition()) { - const msg = this._errorMsg('Signers are only included in epoch transition blocks (clique)') - throw new Error(msg) - } - - const start = CLIQUE_EXTRA_VANITY - const end = this.extraData.length - CLIQUE_EXTRA_SEAL - const signerBytes = this.extraData.subarray(start, end) - - const signerList: Uint8Array[] = [] - const signerLength = 20 - for (let start = 0; start <= signerBytes.length - signerLength; start += signerLength) { - signerList.push(signerBytes.subarray(start, start + signerLength)) - } - return signerList.map((buf) => new Address(buf)) - } - - /** - * Verifies the signature of the block (last 65 bytes of extraData field) - * (only clique PoA, throws otherwise) - * - * Method throws if signature is invalid - */ - cliqueVerifySignature(signerList: Address[]): boolean { - this._requireClique('cliqueVerifySignature') - const signerAddress = this.cliqueSigner() - const signerFound = signerList.find((signer) => { - return signer.equals(signerAddress) - }) - return !!signerFound - } - - /** - * Returns the signer address - */ - cliqueSigner(): Address { - this._requireClique('cliqueSigner') - const extraSeal = this.cliqueExtraSeal() - // Reasonable default for default blocks - if (extraSeal.length === 0 || equalsBytes(extraSeal, new Uint8Array(65))) { - return createZeroAddress() - } - const r = extraSeal.subarray(0, 32) - const s = extraSeal.subarray(32, 64) - const v = bytesToBigInt(extraSeal.subarray(64, 65)) + BIGINT_27 - const pubKey = ecrecover(this.cliqueSigHash(), v, r, s) - return createAddressFromPublicKey(pubKey) - } - /** * Returns the rlp encoding of the block header. */ diff --git a/packages/block/src/index.ts b/packages/block/src/index.ts index c1a94abb64..4ea497e515 100644 --- a/packages/block/src/index.ts +++ b/packages/block/src/index.ts @@ -1,4 +1,5 @@ export { Block } from './block.js' +export * from './consensus/index.js' export * from './constructors.js' export { type BeaconPayloadJson, executionPayloadFromBeaconPayload } from './from-beacon-payload.js' export { BlockHeader } from './header.js' diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 8957546946..27d15a95b2 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -81,11 +81,6 @@ export interface BlockOptions { * Default: true */ freeze?: boolean - /** - * Provide a clique signer's privateKey to seal this block. - * Will throw if provided on a non-PoA chain. - */ - cliqueSigner?: Uint8Array /** * Skip consensus format validation checks on header if set. Defaults to false. */ diff --git a/packages/block/test/clique.spec.ts b/packages/block/test/clique.spec.ts index e7af1e298a..23ad345b01 100644 --- a/packages/block/test/clique.spec.ts +++ b/packages/block/test/clique.spec.ts @@ -2,7 +2,15 @@ import { Common, Goerli, Hardfork } from '@ethereumjs/common' import { Address, createZeroAddress, hexToBytes } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' -import { createBlockHeader } from '../src/constructors.js' +import { createBlockHeader, createSealedCliqueBlockHeader } from '../src/constructors.js' +import { + cliqueEpochTransitionSigners, + cliqueExtraSeal, + cliqueExtraVanity, + cliqueIsEpochTransition, + cliqueSigner, + cliqueVerifySignature, +} from '../src/index.js' describe('[Header]: Clique PoA Functionality', () => { const common = new Common({ chain: Goerli, hardfork: Hardfork.Chainstart }) @@ -11,7 +19,7 @@ describe('[Header]: Clique PoA Functionality', () => { let header = createBlockHeader({ number: 1 }) assert.throws( () => { - header.cliqueIsEpochTransition() + cliqueIsEpochTransition(header) }, undefined, undefined, @@ -20,28 +28,28 @@ describe('[Header]: Clique PoA Functionality', () => { header = createBlockHeader({ extraData: new Uint8Array(97) }, { common }) assert.ok( - header.cliqueIsEpochTransition(), + cliqueIsEpochTransition(header), 'cliqueIsEpochTransition() -> should indicate an epoch transition for the genesis block', ) header = createBlockHeader({ number: 1, extraData: new Uint8Array(97) }, { common }) assert.notOk( - header.cliqueIsEpochTransition(), + cliqueIsEpochTransition(header), 'cliqueIsEpochTransition() -> should correctly identify a non-epoch block', ) assert.deepEqual( - header.cliqueExtraVanity(), + cliqueExtraVanity(header), new Uint8Array(32), 'cliqueExtraVanity() -> should return correct extra vanity value', ) assert.deepEqual( - header.cliqueExtraSeal(), + cliqueExtraSeal(header), new Uint8Array(65), 'cliqueExtraSeal() -> should return correct extra seal value', ) assert.throws( () => { - header.cliqueEpochTransitionSigners() + cliqueEpochTransitionSigners(header) }, undefined, undefined, @@ -50,23 +58,23 @@ describe('[Header]: Clique PoA Functionality', () => { header = createBlockHeader({ number: 60000, extraData: new Uint8Array(137) }, { common }) assert.ok( - header.cliqueIsEpochTransition(), + cliqueIsEpochTransition(header), 'cliqueIsEpochTransition() -> should correctly identify an epoch block', ) assert.deepEqual( - header.cliqueExtraVanity(), + cliqueExtraVanity(header), new Uint8Array(32), 'cliqueExtraVanity() -> should return correct extra vanity value', ) assert.deepEqual( - header.cliqueExtraSeal(), + cliqueExtraSeal(header), new Uint8Array(65), 'cliqueExtraSeal() -> should return correct extra seal value', ) const msg = 'cliqueEpochTransitionSigners() -> should return the correct epoch transition signer list on epoch block' assert.deepEqual( - header.cliqueEpochTransitionSigners(), + cliqueEpochTransitionSigners(header), [createZeroAddress(), createZeroAddress()], msg, ) @@ -87,20 +95,21 @@ describe('[Header]: Clique PoA Functionality', () => { } it('Signing', () => { - const cliqueSigner = A.privateKey + const cliqueSignerKey = A.privateKey - let header = createBlockHeader( + let header = createSealedCliqueBlockHeader( { number: 1, extraData: new Uint8Array(97) }, - { common, freeze: false, cliqueSigner }, + cliqueSignerKey, + { common, freeze: false }, ) assert.equal(header.extraData.length, 97) - assert.ok(header.cliqueVerifySignature([A.address]), 'should verify signature') - assert.ok(header.cliqueSigner().equals(A.address), 'should recover the correct signer address') + assert.ok(cliqueVerifySignature(header, [A.address]), 'should verify signature') + assert.ok(cliqueSigner(header).equals(A.address), 'should recover the correct signer address') header = createBlockHeader({ extraData: new Uint8Array(97) }, { common }) assert.ok( - header.cliqueSigner().equals(createZeroAddress()), + cliqueSigner(header).equals(createZeroAddress()), 'should return zero address on default block', ) }) diff --git a/packages/block/test/difficulty.spec.ts b/packages/block/test/difficulty.spec.ts index 95c9ec2e6d..40fcb27512 100644 --- a/packages/block/test/difficulty.spec.ts +++ b/packages/block/test/difficulty.spec.ts @@ -12,11 +12,10 @@ import * as difficultyFrontier from '../../ethereum-tests/DifficultyTests/dfFron import * as difficultyGrayGlacier from '../../ethereum-tests/DifficultyTests/dfGrayGlacier/difficultyGrayGlacier.json' import * as difficultyHomestead from '../../ethereum-tests/DifficultyTests/dfHomestead/difficultyHomestead.json' import { createBlock } from '../src/constructors.js' - -import type { Block } from '../src/index.js' +import { type Block, ethashCanonicalDifficulty } from '../src/index.js' function runDifficultyTests(test: any, parentBlock: Block, block: Block, msg: string) { - const dif = block.ethashCanonicalDifficulty(parentBlock) + const dif = ethashCanonicalDifficulty(block, parentBlock) assert.equal(dif, BigInt(test.currentDifficulty), `test ethashCanonicalDifficulty: ${msg}`) } diff --git a/packages/blockchain/src/consensus/clique.ts b/packages/blockchain/src/consensus/clique.ts index 8d8a163142..dc7af67ea9 100644 --- a/packages/blockchain/src/consensus/clique.ts +++ b/packages/blockchain/src/consensus/clique.ts @@ -1,3 +1,9 @@ +import { + cliqueEpochTransitionSigners, + cliqueIsEpochTransition, + cliqueSigner, + cliqueVerifySignature, +} from '@ethereumjs/block' import { ConsensusAlgorithm } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' import { @@ -136,7 +142,7 @@ export class CliqueConsensus implements Consensus { } const { header } = block - const valid = header.cliqueVerifySignature(this.cliqueActiveSigners(header.number)) + const valid = cliqueVerifySignature(header, this.cliqueActiveSigners(header.number)) if (!valid) { throw new Error('invalid PoA block signature (clique)') } @@ -145,11 +151,11 @@ export class CliqueConsensus implements Consensus { } // validate checkpoint signers towards active signers on epoch transition blocks - if (header.cliqueIsEpochTransition()) { + if (cliqueIsEpochTransition(header)) { // note: keep votes on epoch transition blocks in case of reorgs. // only active (non-stale) votes will counted (if vote.blockNumber >= lastEpochBlockNumber - const checkpointSigners = header.cliqueEpochTransitionSigners() + const checkpointSigners = cliqueEpochTransitionSigners(header) const activeSigners = this.cliqueActiveSigners(header.number) for (const [i, cSigner] of checkpointSigners.entries()) { if (activeSigners[i]?.equals(cSigner) !== true) { @@ -178,7 +184,7 @@ export class CliqueConsensus implements Consensus { throw new Error(`${msg} ${header.errorStr()}`) } const signerIndex = signers.findIndex((address: Address) => - address.equals(header.cliqueSigner()), + address.equals(cliqueSigner(header)), ) const inTurn = header.number % BigInt(signers.length) === BigInt(signerIndex) if ( @@ -211,7 +217,7 @@ export class CliqueConsensus implements Consensus { private async cliqueSaveGenesisSigners(genesisBlock: Block) { const genesisSignerState: CliqueSignerState = [ BIGINT_0, - genesisBlock.header.cliqueEpochTransitionSigners(), + cliqueEpochTransitionSigners(genesisBlock.header), ] await this.cliqueUpdateSignerStates(genesisSignerState) debug(`[Block 0] Genesis block -> update signer states`) @@ -280,7 +286,7 @@ export class CliqueConsensus implements Consensus { private async cliqueUpdateVotes(header?: BlockHeader) { // Block contains a vote on a new signer if (header && !header.coinbase.isZero()) { - const signer = header.cliqueSigner() + const signer = cliqueSigner(header) const beneficiary = header.coinbase const nonce = header.nonce const latestVote: CliqueVote = [header.number, [signer, beneficiary, nonce]] @@ -469,8 +475,8 @@ export class CliqueConsensus implements Consensus { // we do not have a complete picture of the state to verify if too recently signed return false } - signers.push([header.number, header.cliqueSigner()]) - const seen = signers.filter((s) => s[1].equals(header.cliqueSigner())).length + signers.push([header.number, cliqueSigner(header)]) + const seen = signers.filter((s) => s[1].equals(cliqueSigner(header))).length return seen > 1 } @@ -509,7 +515,7 @@ export class CliqueConsensus implements Consensus { return } // add this block's signer - const signer: CliqueBlockSigner = [header.number, header.cliqueSigner()] + const signer: CliqueBlockSigner = [header.number, cliqueSigner(header)] this._cliqueLatestBlockSigners.push(signer) // trim length to `this.cliqueSignerLimit()` @@ -587,7 +593,7 @@ export class CliqueConsensus implements Consensus { * @hidden */ private async _cliqueBuildSnapshots(header: BlockHeader) { - if (!header.cliqueIsEpochTransition()) { + if (!cliqueIsEpochTransition(header)) { await this.cliqueUpdateVotes(header) } await this.cliqueUpdateLatestBlockSigners(header) diff --git a/packages/blockchain/test/clique.spec.ts b/packages/blockchain/test/clique.spec.ts index 3248be5ca5..f4307ac872 100644 --- a/packages/blockchain/test/clique.spec.ts +++ b/packages/blockchain/test/clique.spec.ts @@ -1,4 +1,8 @@ -import { createBlock } from '@ethereumjs/block' +import { + cliqueEpochTransitionSigners, + createBlock, + createSealedCliqueBlock, +} from '@ethereumjs/block' import { Common, ConsensusAlgorithm, @@ -155,7 +159,7 @@ function getBlock( // set signer const cliqueSigner = signer.privateKey - return createBlock(blockData, { common, freeze: false, cliqueSigner }) + return createSealedCliqueBlock(blockData, cliqueSigner, { common }) } const addNextBlockReorg = async ( @@ -206,7 +210,7 @@ describe('Clique: Initialization', () => { assert.deepEqual( (blockchain.consensus as CliqueConsensus).cliqueActiveSigners(head.header.number + BigInt(1)), - head.header.cliqueEpochTransitionSigners(), + cliqueEpochTransitionSigners(head.header), 'correct genesis signers', ) }) @@ -224,10 +228,10 @@ describe('Clique: Initialization', () => { unauthorizedSigner.toBytes(), new Uint8Array(65), ) - const block = createBlock( - { header: { number, extraData } }, - { common: COMMON, cliqueSigner: A.privateKey }, - ) + const block = createSealedCliqueBlock({ header: { number, extraData } }, A.privateKey, { + common: COMMON, + freeze: false, + }) try { await blockchain.putBlock(block) assert.fail('should fail') @@ -271,7 +275,7 @@ describe('Clique: Initialization', () => { difficulty = BigInt(1) const cliqueSigner = A.privateKey - block = createBlock( + block = createSealedCliqueBlock( { header: { number, @@ -281,7 +285,8 @@ describe('Clique: Initialization', () => { timestamp: parentHeader.timestamp + BigInt(10000), }, }, - { common: COMMON, cliqueSigner }, + cliqueSigner, + { common: COMMON }, ) try { diff --git a/packages/blockchain/test/reorg.spec.ts b/packages/blockchain/test/reorg.spec.ts index 855725d6c1..19382953b9 100644 --- a/packages/blockchain/test/reorg.spec.ts +++ b/packages/blockchain/test/reorg.spec.ts @@ -1,4 +1,4 @@ -import { createBlock } from '@ethereumjs/block' +import { cliqueSigner, createBlock } from '@ethereumjs/block' import { Common, ConsensusAlgorithm, Goerli, Hardfork, Mainnet } from '@ethereumjs/common' import { Address, equalsBytes, hexToBytes } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' @@ -168,7 +168,7 @@ describe('reorg tests', () => { !signerVotes.find( (v: any) => v[0] === BigInt(2) && - v[1][0].equal(block1_low.header.cliqueSigner()) && + v[1][0].equal(cliqueSigner(block1_low.header)) && v[1][1].equal(beneficiary1) && equalsBytes(v[1][2], CLIQUE_NONCE_AUTH), ), @@ -178,7 +178,7 @@ describe('reorg tests', () => { let blockSigners = (blockchain.consensus as CliqueConsensus)._cliqueLatestBlockSigners assert.ok( !blockSigners.find( - (s: any) => s[0] === BigInt(1) && s[1].equal(block1_low.header.cliqueSigner()), + (s: any) => s[0] === BigInt(1) && s[1].equal(cliqueSigner(block1_low.header)), ), 'should not find reorged block signer', ) @@ -197,7 +197,7 @@ describe('reorg tests', () => { blockSigners = (blockchain.consensus as CliqueConsensus)._cliqueLatestBlockSigners assert.ok( !!blockSigners.find( - (s: any) => s[0] === BigInt(3) && s[1].equals(block3_high.header.cliqueSigner()), + (s: any) => s[0] === BigInt(3) && s[1].equals(cliqueSigner(block3_high.header)), ), 'should find reorged block signer', ) diff --git a/packages/client/src/miner/miner.ts b/packages/client/src/miner/miner.ts index d4acf00e96..271c92b92d 100644 --- a/packages/client/src/miner/miner.ts +++ b/packages/client/src/miner/miner.ts @@ -1,4 +1,4 @@ -import { type BlockHeader, createBlockHeader } from '@ethereumjs/block' +import { type BlockHeader, createSealedCliqueBlockHeader } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' import { Ethash } from '@ethereumjs/ethash' import { BIGINT_0, BIGINT_1, BIGINT_2, bytesToHex, equalsBytes } from '@ethereumjs/util' @@ -208,10 +208,10 @@ export class Miner { if (this.config.chainCommon.consensusType() === ConsensusType.ProofOfAuthority) { // Abort if we have too recently signed const cliqueSigner = this.config.accounts[0][1] - const header = createBlockHeader( - { number }, - { common: this.config.chainCommon, cliqueSigner }, - ) + const header = createSealedCliqueBlockHeader({ number }, cliqueSigner, { + common: this.config.chainCommon, + freeze: false, + }) if ( (this.service.chain.blockchain as any).consensus.cliqueCheckRecentlySigned(header) === true ) { diff --git a/packages/client/test/integration/miner.spec.ts b/packages/client/test/integration/miner.spec.ts index 07239d73c1..f9dd101829 100644 --- a/packages/client/test/integration/miner.spec.ts +++ b/packages/client/test/integration/miner.spec.ts @@ -82,39 +82,40 @@ async function minerSetup(): Promise<[MockServer, FullEthereumService]> { return [server, service] } -describe( - 'should mine blocks while a peer stays connected to tip of chain', - async () => { - const [server, service] = await minerSetup() - const [remoteServer, remoteService] = await setup({ - location: '127.0.0.2', - height: 0, - common, - }) - ;(remoteService.chain.blockchain.consensus as CliqueConsensus).cliqueActiveSigners = () => [ - accounts[0][0], - ] // stub - ;(remoteService as FullEthereumService).execution.run = async () => 1 // stub - await server.discover('remotePeer1', '127.0.0.2') - const targetHeight = BigInt(5) - await new Promise((resolve) => { - remoteService.config.events.on(Event.SYNC_SYNCHRONIZED, async (chainHeight) => { - if (chainHeight === targetHeight) { - it('should sync blocks', () => { +describe('should mine blocks while a peer stays connected to tip of chain', () => { + it( + 'should work', + async () => { + const [server, service] = await minerSetup() + const [remoteServer, remoteService] = await setup({ + location: '127.0.0.2', + height: 0, + common, + }) + ;(remoteService.chain.blockchain.consensus as CliqueConsensus).cliqueActiveSigners = () => [ + accounts[0][0], + ] // stub + ;(remoteService as FullEthereumService).execution.run = async () => 1 // stub + await server.discover('remotePeer1', '127.0.0.2') + const targetHeight = BigInt(5) + await new Promise((resolve) => { + remoteService.config.events.on(Event.SYNC_SYNCHRONIZED, async (chainHeight) => { + if (chainHeight === targetHeight) { assert.equal( remoteService.chain.blocks.height, targetHeight, 'synced blocks successfully', ) - }) - await destroy(server, service) - await destroy(remoteServer, remoteService) - resolve(undefined) - void remoteService.synchronizer!.start() - } + await destroy(server, service) + await destroy(remoteServer, remoteService) + resolve(undefined) + + void remoteService.synchronizer!.start() + } + }) }) - }) - }, - { timeout: 25000 }, -) + }, + { timeout: 25000 }, + ) +}) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index ec96b35180..d76a7024f9 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -1149,7 +1149,6 @@ export function defaultBlock(): Block { return { header: { number: BIGINT_0, - cliqueSigner: () => createZeroAddress(), coinbase: createZeroAddress(), timestamp: BIGINT_0, difficulty: BIGINT_0, diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index 2ea57847d2..0a0b153cbf 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -157,6 +157,15 @@ export class Interpreter { this._evm = evm this._stateManager = stateManager this.common = this._evm.common + + if ( + this.common.consensusType() === 'poa' && + this._evm['_optsCached'].cliqueSigner === undefined + ) + throw new Error( + 'Must include cliqueSigner function if clique/poa is being used for consensus type', + ) + this._runState = { programCounter: 0, opCode: 0xfe, // INVALID opcode @@ -780,7 +789,7 @@ export class Interpreter { getBlockCoinbase(): bigint { let coinbase: Address if (this.common.consensusAlgorithm() === ConsensusAlgorithm.Clique) { - coinbase = this._env.block.header.cliqueSigner() + coinbase = this._evm['_optsCached'].cliqueSigner!(this._env.block.header) } else { coinbase = this._env.block.header.coinbase } diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index f385929e55..b5c0dac660 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -345,6 +345,13 @@ export interface EVMOpts { * */ profiler?: EVMProfilerOpts + + /** + * When running the EVM with PoA consensus, the `cliqueSigner` function from the `@ethereumjs/block` class + * must be provided along with a `BlockHeader` so that the coinbase can be correctly retrieved when the + * `Interpreter.getBlockCoinbase` method is called. + */ + cliqueSigner?: (header: Block['header']) => Address } /** @@ -439,7 +446,6 @@ export type Log = [address: Uint8Array, topics: Uint8Array[], data: Uint8Array] export type Block = { header: { number: bigint - cliqueSigner(): Address coinbase: Address timestamp: bigint difficulty: bigint diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index fa3080afaf..82604b573d 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -1,5 +1,6 @@ import { createBlock, + createSealedCliqueBlock, genRequestsTrieRoot, genTransactionsTrieRoot, genWithdrawalsTrieRoot, @@ -363,7 +364,13 @@ export class BlockBuilder { requests, } - const block = createBlock(blockData, blockOpts) + let block + const cs = this.blockOpts.cliqueSigner + if (cs !== undefined) { + block = createSealedCliqueBlock(blockData, cs, this.blockOpts) + } else { + block = createBlock(blockData, blockOpts) + } if (this.blockOpts.putBlockIntoBlockchain === true) { await this.vm.blockchain.putBlock(block) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 6832b64028..b30bd7b465 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -1,4 +1,4 @@ -import { createBlock } from '@ethereumjs/block' +import { cliqueSigner, createBlock } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' import { StatelessVerkleStateManager } from '@ethereumjs/statemanager' @@ -589,7 +589,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { // Update miner's balance let miner if (vm.common.consensusType() === ConsensusType.ProofOfAuthority) { - miner = block.header.cliqueSigner() + miner = cliqueSigner(block.header) } else { miner = block.header.coinbase } diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index 16e59c1bd7..cb736c3a0c 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -195,6 +195,11 @@ export interface BuilderOpts extends BlockOptions { * Default: true */ putBlockIntoBlockchain?: boolean + /** + * Provide a clique signer's privateKey to seal this block. + * Will throw if provided on a non-PoA chain. + */ + cliqueSigner?: Uint8Array } /** diff --git a/packages/vm/test/api/buildBlock.spec.ts b/packages/vm/test/api/buildBlock.spec.ts index 2cb480d1c1..6c1a8d159e 100644 --- a/packages/vm/test/api/buildBlock.spec.ts +++ b/packages/vm/test/api/buildBlock.spec.ts @@ -1,4 +1,9 @@ -import { createBlock } from '@ethereumjs/block' +import { + cliqueSigner, + cliqueVerifySignature, + createBlock, + createSealedCliqueBlock, +} from '@ethereumjs/block' import { EthashConsensus, createBlockchain } from '@ethereumjs/blockchain' import { Common, @@ -198,10 +203,11 @@ describe('BlockBuilder', () => { // extraData: [vanity, activeSigner, seal] const extraData = concatBytes(new Uint8Array(32), signer.address.toBytes(), new Uint8Array(65)) - const cliqueSigner = signer.privateKey - const genesisBlock = createBlock( + const cliqueSignerKey = signer.privateKey + const genesisBlock = createSealedCliqueBlock( { header: { gasLimit: 50000, extraData } }, - { common, cliqueSigner }, + cliqueSignerKey, + { common }, ) const blockchain = await createBlockchain({ genesisBlock, common }) const vm = await VM.create({ common, blockchain }) @@ -212,7 +218,7 @@ describe('BlockBuilder', () => { const blockBuilder = await buildBlock(vm, { parentBlock: genesisBlock, headerData: { difficulty: 2, extraData: new Uint8Array(97) }, - blockOpts: { cliqueSigner, freeze: false }, + blockOpts: { cliqueSigner: cliqueSignerKey, freeze: false }, }) // Set up tx @@ -225,9 +231,9 @@ describe('BlockBuilder', () => { const block = await blockBuilder.build() - assert.ok(block.header.cliqueVerifySignature([signer.address]), 'should verify signature') + assert.ok(cliqueVerifySignature(block.header, [signer.address]), 'should verify signature') assert.deepEqual( - block.header.cliqueSigner(), + cliqueSigner(block.header), signer.address, 'should recover the correct signer address', ) diff --git a/packages/vm/test/api/runBlock.spec.ts b/packages/vm/test/api/runBlock.spec.ts index 1d99c14640..e5a52109a9 100644 --- a/packages/vm/test/api/runBlock.spec.ts +++ b/packages/vm/test/api/runBlock.spec.ts @@ -2,6 +2,7 @@ import { createBlock, createBlockFromRLPSerializedBlock, createBlockFromValuesArray, + createSealedCliqueBlock, } from '@ethereumjs/block' import { Common, Goerli, Hardfork, Mainnet, createCustomCommon } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' @@ -387,9 +388,10 @@ describe('runBlock() -> runtime behavior', async () => { ).sign(otherUser.privateKey) // create block with the signer and txs - const block = createBlock( + const block = createSealedCliqueBlock( { header: { extraData: new Uint8Array(97) }, transactions: [tx, tx] }, - { common, cliqueSigner: signer.privateKey }, + signer.privateKey, + { common }, ) await runBlock(vm, { block, skipNonce: true, skipBlockValidation: true, generate: true })