From d6d9391f1fdfc259baf3150523ecd7f27d2a9373 Mon Sep 17 00:00:00 2001 From: Holger Drewes Date: Wed, 30 Aug 2023 16:22:33 +0200 Subject: [PATCH] Common: Cache Parameter Values + activated EIPs for current Hardfork / SM copy() fix (#2994) * VM: add total test time tracking to blockchain test runner * Simplify on paramByHardfork(), paramByEIP() reads, replace with param() usage * Common: make private members protected for some greater flexibility for users on sub-classing * Common: new _buildParamsCache() methods + entrypoint calls, new _paramsCache member * Common: add _buildParamsCache() implementation, replace param() code with direct _paramsCache() access * Small fix * VM: fix precompile activation API test * StateManager: fix Common not properly copied within shallowCopy() method * Common: add additional param() test for copied/original Common * Common: fix cache initialization in params cache build method * Common: add activated EIPs cache * Apply review suggestions --- packages/block/src/header.ts | 27 ++-- packages/common/src/common.ts | 141 ++++++++++++++---- packages/common/src/types.ts | 2 +- packages/common/test/params.spec.ts | 16 ++ .../precompiles/0a-kzg-point-evaluation.ts | 4 +- packages/statemanager/src/stateManager.ts | 4 + packages/tx/src/eip4844Transaction.ts | 6 +- packages/vm/test/tester/index.ts | 5 +- 8 files changed, 153 insertions(+), 52 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 0d7ab1790b..a259a9ffd1 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -404,14 +404,13 @@ export class BlockHeader { */ protected _consensusFormatValidation() { const { nonce, uncleHash, difficulty, extraData, number } = this - const hardfork = this.common.hardfork() // Consensus type dependent checks if (this.common.consensusAlgorithm() === ConsensusAlgorithm.Ethash) { // PoW/Ethash if ( number > BigInt(0) && - this.extraData.length > this.common.paramByHardfork('vm', 'maxExtraDataSize', hardfork) + this.extraData.length > this.common.param('vm', 'maxExtraDataSize') ) { // Check length of data on all post-genesis blocks const msg = this._errorMsg('invalid amount of extra data') @@ -506,10 +505,8 @@ export class BlockHeader { parentGasLimit = parentGasLimit * elasticity } const gasLimit = this.gasLimit - const hardfork = this.common.hardfork() - const a = - parentGasLimit / this.common.paramByHardfork('gasConfig', 'gasLimitBoundDivisor', hardfork) + const a = parentGasLimit / this.common.param('gasConfig', 'gasLimitBoundDivisor') const maxGasLimit = parentGasLimit + a const minGasLimit = parentGasLimit - a @@ -523,10 +520,8 @@ export class BlockHeader { throw new Error(msg) } - if (gasLimit < this.common.paramByHardfork('gasConfig', 'minGasLimit', hardfork)) { - const msg = this._errorMsg( - `gas limit decreased below minimum gas limit for hardfork=${hardfork}` - ) + if (gasLimit < this.common.param('gasConfig', 'minGasLimit')) { + const msg = this._errorMsg(`gas limit decreased below minimum gas limit`) throw new Error(msg) } } @@ -704,18 +699,16 @@ export class BlockHeader { ) throw new Error(msg) } - const hardfork = this.common.hardfork() const blockTs = this.timestamp const { timestamp: parentTs, difficulty: parentDif } = parentBlockHeader - const minimumDifficulty = this.common.paramByHardfork('pow', 'minimumDifficulty', hardfork) - const offset = - parentDif / this.common.paramByHardfork('pow', 'difficultyBoundDivisor', hardfork) + const minimumDifficulty = this.common.param('pow', 'minimumDifficulty') + const offset = parentDif / this.common.param('pow', 'difficultyBoundDivisor') let num = this.number // We use a ! here as TS cannot follow this hardfork-dependent logic, but it always gets assigned let dif!: bigint - if (this.common.hardforkGteHardfork(hardfork, Hardfork.Byzantium) === true) { + if (this.common.gteHardfork(Hardfork.Byzantium) === true) { // max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99) (EIP100) const uncleAddend = equalsBytes(parentBlockHeader.uncleHash, KECCAK256_RLP_ARRAY) ? 1 : 2 let a = BigInt(uncleAddend) - (blockTs - parentTs) / BigInt(9) @@ -727,13 +720,13 @@ export class BlockHeader { dif = parentDif + offset * a } - if (this.common.hardforkGteHardfork(hardfork, Hardfork.Byzantium) === true) { + if (this.common.gteHardfork(Hardfork.Byzantium) === true) { // Get delay as parameter from common num = num - this.common.param('pow', 'difficultyBombDelay') if (num < BigInt(0)) { num = BigInt(0) } - } else if (this.common.hardforkGteHardfork(hardfork, Hardfork.Homestead) === true) { + } else if (this.common.gteHardfork(Hardfork.Homestead) === true) { // 1 - (block_timestamp - parent_timestamp) // 10 let a = BigInt(1) - (blockTs - parentTs) / BigInt(10) const cutoff = BigInt(-99) @@ -744,7 +737,7 @@ export class BlockHeader { dif = parentDif + offset * a } else { // pre-homestead - if (parentTs + this.common.paramByHardfork('pow', 'durationLimit', hardfork) > blockTs) { + if (parentTs + this.common.param('pow', 'durationLimit') > blockTs) { dif = offset + parentDif } else { dif = parentDif - offset diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 890de04692..93a26edd9f 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -25,16 +25,22 @@ import type { CliqueConfig, CommonOpts, CustomCommonOpts, + EIPConfig, + EIPOrHFConfig, EthashConfig, GenesisBlockConfig, GethConfigOpts, HardforkByOpts, + HardforkConfig, HardforkTransitionConfig, } from './types.js' import type { BigIntLike, PrefixedHexString } from '@ethereumjs/util' type HardforkSpecKeys = string // keyof typeof HARDFORK_SPECS type HardforkSpecValues = typeof HARDFORK_SPECS[HardforkSpecKeys] + +type ParamsCacheConfig = Omit + /** * Common class to access chain and hardfork parameters and to provide * a unified and shared view on the network and hardfork state. @@ -46,12 +52,15 @@ type HardforkSpecValues = typeof HARDFORK_SPECS[HardforkSpecKeys] export class Common { readonly DEFAULT_HARDFORK: string | Hardfork - private _chainParams: ChainConfig - private _hardfork: string | Hardfork - private _eips: number[] = [] - private _customChains: ChainConfig[] + protected _chainParams: ChainConfig + protected _hardfork: string | Hardfork + protected _eips: number[] = [] + protected _customChains: ChainConfig[] - private HARDFORK_CHANGES: [HardforkSpecKeys, HardforkSpecValues][] + protected _paramsCache: ParamsCacheConfig = {} + protected _activatedEIPsCache: number[] = [] + + protected HARDFORK_CHANGES: [HardforkSpecKeys, HardforkSpecValues][] public events: EventEmitter @@ -197,7 +206,7 @@ export class Common { return Boolean((initializedChains['names'] as ChainName)[chainId.toString()]) } - private static _getChainParams( + protected static _getChainParams( chain: string | number | Chain | bigint, customChains?: ChainConfig[] ): ChainConfig { @@ -238,6 +247,10 @@ export class Common { if (opts.eips) { this.setEIPs(opts.eips) } + if (Object.keys(this._paramsCache).length === 0) { + this._buildParamsCache() + this._buildActivatedEIPsCache() + } } /** @@ -283,6 +296,8 @@ export class Common { if (hfChanges[0] === hardfork) { if (this._hardfork !== hardfork) { this._hardfork = hardfork + this._buildParamsCache() + this._buildActivatedEIPsCache() this.events.emit('hardforkChanged', hardfork) } existing = true @@ -435,7 +450,7 @@ export class Common { * @param hardfork Hardfork name * @returns Dictionary with hardfork params or null if hardfork not on chain */ - private _getHardfork(hardfork: string | Hardfork): HardforkTransitionConfig | null { + protected _getHardfork(hardfork: string | Hardfork): HardforkTransitionConfig | null { const hfs = this.hardforks() for (const hf of hfs) { if (hf['name'] === hardfork) return hf @@ -458,6 +473,12 @@ export class Common { `${eip} cannot be activated on hardfork ${this.hardfork()}, minimumHardfork: ${minHF}` ) } + } + this._eips = eips + this._buildParamsCache() + this._buildActivatedEIPsCache() + + for (const eip of eips) { if ((EIPs as any)[eip].requiredEIPs !== undefined) { for (const elem of (EIPs as any)[eip].requiredEIPs) { if (!(eips.includes(elem) || this.isActivatedEIP(elem))) { @@ -466,14 +487,85 @@ export class Common { } } } - this._eips = eips + } + + /** + * Internal helper for _buildParamsCache() + */ + protected _mergeWithParamsCache(params: HardforkConfig | EIPConfig) { + this._paramsCache['gasConfig'] = { + ...this._paramsCache['gasConfig'], + ...params['gasConfig'], + } + this._paramsCache['gasPrices'] = { + ...this._paramsCache['gasPrices'], + ...params['gasPrices'], + } + this._paramsCache['pow'] = { + ...this._paramsCache['pow'], + ...params['pow'], + } + this._paramsCache['sharding'] = { + ...this._paramsCache['sharding'], + ...params['sharding'], + } + this._paramsCache['vm'] = { + ...this._paramsCache['vm'], + ...params['vm'], + } + } + + /** + * Build up a cache for all parameter values for the current HF and all activated EIPs + */ + protected _buildParamsCache() { + this._paramsCache = {} + // Iterate through all hardforks up to hardfork set + const hardfork = this.hardfork() + for (const hfChanges of this.HARDFORK_CHANGES) { + // EIP-referencing HF config (e.g. for berlin) + if ('eips' in hfChanges[1]) { + const hfEIPs = hfChanges[1]['eips'] + for (const eip of hfEIPs!) { + if (!(eip in EIPs)) { + throw new Error(`${eip} not supported`) + } + + this._mergeWithParamsCache(EIPs[eip]) + } + // Parameter-inlining HF config (e.g. for istanbul) + } else { + this._mergeWithParamsCache(hfChanges[1]) + } + if (hfChanges[0] === hardfork) break + } + // Iterate through all additionally activated EIPs + for (const eip of this._eips) { + if (!(eip in EIPs)) { + throw new Error(`${eip} not supported`) + } + + this._mergeWithParamsCache(EIPs[eip]) + } + } + + protected _buildActivatedEIPsCache() { + this._activatedEIPsCache = [] + + for (const hfChanges of this.HARDFORK_CHANGES) { + const hf = hfChanges[1] + if (this.gteHardfork(hf['name']) && 'eips' in hf) { + this._activatedEIPsCache = this._activatedEIPsCache.concat(hf['eips'] as number[]) + } + } + this._activatedEIPsCache = this._activatedEIPsCache.concat(this._eips) } /** * Returns a parameter for the current chain setup * * If the parameter is present in an EIP, the EIP always takes precedence. - * Otherwise the parameter if taken from the latest applied HF with + * Otherwise the parameter is taken from the latest applied HF with * a change on the respective parameter. * * @param topic Parameter topic ('gasConfig', 'gasPrices', 'vm', 'pow') @@ -483,12 +575,14 @@ export class Common { param(topic: string, name: string): bigint { // TODO: consider the case that different active EIPs // can change the same parameter - let value - for (const eip of this._eips) { - value = this.paramByEIP(topic, name, eip) - if (value !== undefined) return value + let value = null + if ( + (this._paramsCache as any)[topic] !== undefined && + (this._paramsCache as any)[topic][name] !== undefined + ) { + value = (this._paramsCache as any)[topic][name].v } - return this.paramByHardfork(topic, name, this._hardfork) + return BigInt(value ?? 0) } /** @@ -501,14 +595,14 @@ export class Common { paramByHardfork(topic: string, name: string, hardfork: string | Hardfork): bigint { let value = null for (const hfChanges of this.HARDFORK_CHANGES) { - // EIP-referencing HF file (e.g. berlin.json) + // EIP-referencing HF config (e.g. for berlin) if ('eips' in hfChanges[1]) { const hfEIPs = hfChanges[1]['eips'] for (const eip of hfEIPs!) { const valueEIP = this.paramByEIP(topic, name, eip) value = typeof valueEIP === 'bigint' ? valueEIP : value } - // Parameter-inlining HF file (e.g. istanbul.json) + // Parameter-inlining HF config (e.g. for istanbul) } else { if ( (hfChanges[1] as any)[topic] !== undefined && @@ -575,17 +669,9 @@ export class Common { * @param eip */ isActivatedEIP(eip: number): boolean { - if (this.eips().includes(eip)) { + if (this._activatedEIPsCache.includes(eip)) { return true } - for (const hfChanges of this.HARDFORK_CHANGES) { - const hf = hfChanges[1] - if (this.gteHardfork(hf['name']) && 'eips' in hf) { - if ((hf['eips'] as number[]).includes(eip)) { - return true - } - } - } return false } @@ -755,7 +841,7 @@ export class Common { * @param genesisHash Genesis block hash of the chain * @returns Fork hash as hex string */ - private _calcForkHash(hardfork: string | Hardfork, genesisHash: Uint8Array): PrefixedHexString { + protected _calcForkHash(hardfork: string | Hardfork, genesisHash: Uint8Array): PrefixedHexString { let hfBytes = new Uint8Array(0) let prevBlockOrTime = 0 for (const hf of this.hardforks()) { @@ -905,7 +991,8 @@ export class Common { } /** - * Returns the active EIPs + * Returns the additionally activated EIPs + * (by using the `eips` constructor option) * @returns List of EIPs */ eips(): number[] { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index c882be2bc4..00b4908d55 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -133,7 +133,7 @@ type ParamDict = { d: string } -type EIPOrHFConfig = { +export type EIPOrHFConfig = { comment: string url: string status: string diff --git a/packages/common/test/params.spec.ts b/packages/common/test/params.spec.ts index f82d24918e..83d55e82df 100644 --- a/packages/common/test/params.spec.ts +++ b/packages/common/test/params.spec.ts @@ -80,6 +80,22 @@ describe('[Common]: Parameter access for param(), paramByHardfork()', () => { ) }) + it('Access on copied Common instances', () => { + const c = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai }) + let msg = 'Should correctly access param with param() on original Common' + assert.equal(c.param('pow', 'minerReward'), BigInt(2000000000000000000), msg) + + const cCopy = c.copy() + cCopy.setHardfork(Hardfork.Chainstart) + + msg = 'Should correctly access param with param() on copied Common with hardfork changed' + assert.equal(cCopy.param('pow', 'minerReward'), BigInt(5000000000000000000), msg) + + msg = + 'Should correctly access param with param() on original Common after copy and HF change on copied Common' + assert.equal(c.param('pow', 'minerReward'), BigInt(2000000000000000000), msg) + }) + it('EIP param access, paramByEIP()', () => { const c = new Common({ chain: Chain.Mainnet }) diff --git a/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts b/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts index 918ecd3205..9b00ef5569 100644 --- a/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts +++ b/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts @@ -41,8 +41,8 @@ export async function precompile0a(opts: PrecompileInput): Promise { return EvmErrorResult(new EvmError(ERROR.INVALID_INPUT_LENGTH), opts.gasLimit) } - const version = Number(opts.common.paramByEIP('sharding', 'blobCommitmentVersionKzg', 4844)) - const fieldElementsPerBlob = opts.common.paramByEIP('sharding', 'fieldElementsPerBlob', 4844)! + const version = Number(opts.common.param('sharding', 'blobCommitmentVersionKzg')) + const fieldElementsPerBlob = opts.common.param('sharding', 'fieldElementsPerBlob') const versionedHash = opts.data.subarray(0, 32) const z = opts.data.subarray(32, 64) const y = opts.data.subarray(64, 96) diff --git a/packages/statemanager/src/stateManager.ts b/packages/statemanager/src/stateManager.ts index a8f842dff2..2893d0b5fd 100644 --- a/packages/statemanager/src/stateManager.ts +++ b/packages/statemanager/src/stateManager.ts @@ -926,6 +926,9 @@ export class DefaultStateManager implements EVMStateManagerInterface { * 2. Cache values are generally not copied along */ shallowCopy(): DefaultStateManager { + const common = this.common.copy() + common.setHardfork(this.common.hardfork()) + const trie = this._trie.shallowCopy(false) const prefixCodeHashes = this._prefixCodeHashes let accountCacheOpts = { ...this._accountCacheSettings } @@ -938,6 +941,7 @@ export class DefaultStateManager implements EVMStateManagerInterface { } return new DefaultStateManager({ + common, trie, prefixCodeHashes, accountCacheOpts, diff --git a/packages/tx/src/eip4844Transaction.ts b/packages/tx/src/eip4844Transaction.ts index f511fc83f2..55fb00d840 100644 --- a/packages/tx/src/eip4844Transaction.ts +++ b/packages/tx/src/eip4844Transaction.ts @@ -162,9 +162,7 @@ export class BlobEIP4844Transaction extends BaseTransaction { let testIdentifier: string const failingTests: Record = {} - ;(t as any).on('result', (o: any) => { if ( typeof o.ok !== 'undefined' && @@ -218,6 +217,7 @@ async function runTests() { // https://github.com/ethereum/tests/releases/tag/v7.0.0-beta.1 const dirs = getTestDirs(FORK_CONFIG_VM, name) + console.time('Total (including setup)') for (const dir of dirs) { await new Promise((resolve, reject) => { if (argv.customTestsPath !== undefined) { @@ -263,6 +263,9 @@ async function runTests() { t.ok(assertCount >= expectedTests, `expected ${expectedTests} checks, got ${assertCount}`) } + console.log() + console.timeEnd('Total (including setup)') + t.end() }) }