@ethereumjs/common
Version:
Resources common to all Ethereum implementations
324 lines (294 loc) • 11.7 kB
text/typescript
import {
EthereumJSErrorWithoutCode,
addHexPrefix,
intToHex,
isHexString,
stripHexPrefix,
} from '@ethereumjs/util'
import { Holesky, Hoodi, Kaustinen6, Mainnet, Sepolia } from './chains.ts'
import { Hardfork } from './enums.ts'
import { hardforksDict } from './hardforks.ts'
import type { PrefixedHexString } from '@ethereumjs/util'
import type { GethGenesis } from './gethGenesis.ts'
import type { HardforksDict } from './types.ts'
type ConfigHardfork =
| { name: string; block: null; timestamp: number }
| { name: string; block: number; timestamp?: number }
/**
* Transforms Geth formatted nonce (i.e. hex string) to 8 byte 0x-prefixed string used internally
* @param nonce string parsed from the Geth genesis file
* @returns nonce as a 0x-prefixed 8 byte string
*/
function formatNonce(nonce: string): PrefixedHexString {
if (!nonce || nonce === '0x0') {
return '0x0000000000000000'
}
if (isHexString(nonce)) {
return `0x${stripHexPrefix(nonce).padStart(16, '0')}`
}
return `0x${nonce.padStart(16, '0')}`
}
/**
* Converts Geth genesis parameters to an EthereumJS compatible `CommonOpts` object
* @param gethGenesis GethGenesis object
* @returns genesis parameters in a `CommonOpts` compliant object
*/
function parseGethParams(gethGenesis: GethGenesis) {
const {
name,
config,
difficulty,
mixHash,
gasLimit,
coinbase,
baseFeePerGas,
excessBlobGas,
requestsHash,
extraData: unparsedExtraData,
nonce: unparsedNonce,
timestamp: unparsedTimestamp,
} = gethGenesis
const genesisTimestamp = Number(unparsedTimestamp)
const { chainId, depositContractAddress } = config
// geth is not strictly putting empty fields with a 0x prefix
const extraData = addHexPrefix(unparsedExtraData ?? '')
// geth may use number for timestamp
const timestamp: PrefixedHexString = isHexString(unparsedTimestamp)
? unparsedTimestamp
: intToHex(parseInt(unparsedTimestamp))
// geth may not give us a nonce strictly formatted to an 8 byte 0x-prefixed hex string
const nonce =
unparsedNonce.length !== 18 ? formatNonce(unparsedNonce) : addHexPrefix(unparsedNonce)
// EIP155 and EIP158 are both part of Spurious Dragon hardfork and must occur at the same time
// but have different configuration parameters in geth genesis parameters
if (config.eip155Block !== config.eip158Block) {
throw EthereumJSErrorWithoutCode(
'EIP155 block number must equal EIP 158 block number since both are part of SpuriousDragon hardfork and the client only supports activating the full hardfork',
)
}
let customHardforks: HardforksDict | undefined = undefined
if (config.blobSchedule !== undefined) {
customHardforks = {}
const blobGasPerBlob = 131072
for (const [hfKey, hfSchedule] of Object.entries(config.blobSchedule)) {
const hfConfig = hardforksDict[hfKey]
if (hfConfig === undefined) {
throw EthereumJSErrorWithoutCode(`unknown hardfork=${hfKey} specified in blobSchedule`)
}
const { target, max, baseFeeUpdateFraction: blobGasPriceUpdateFraction } = hfSchedule
if (target === undefined || max === undefined || blobGasPriceUpdateFraction === undefined) {
throw EthereumJSErrorWithoutCode(
`undefined target, max or baseFeeUpdateFraction specified in blobSchedule for hardfork=${hfKey}`,
)
}
// copy current hardfork info to custom and add blob config
const customHfConfig = JSON.parse(JSON.stringify(hfConfig))
customHfConfig.params = {
...customHardforks.params,
// removes blobGasPriceUpdateFraction key to prevent undefined overriding if undefined
...{
targetBlobGasPerBlock: blobGasPerBlob * target,
maxBlobGasPerBlock: blobGasPerBlob * max,
blobGasPriceUpdateFraction,
},
}
customHardforks[hfKey] = customHfConfig
}
}
const params = {
name,
chainId,
depositContractAddress,
genesis: {
timestamp,
gasLimit,
difficulty,
nonce,
extraData,
mixHash,
coinbase,
baseFeePerGas,
excessBlobGas,
requestsHash,
},
hardfork: undefined as string | undefined,
hardforks: [] as ConfigHardfork[],
customHardforks,
bootstrapNodes: [],
consensus:
config.clique !== undefined
? {
type: 'poa',
algorithm: 'clique',
clique: {
// The recent geth genesis seems to be using blockperiodseconds // cspell:disable-line
// and epochlength for clique specification
// see: https://hackmd.io/PqZgMpnkSWCWv5joJoFymQ
period: config.clique.period ?? config.clique.blockperiodseconds, // cspell:disable-line
epoch: config.clique.epoch ?? config.clique.epochlength,
},
}
: {
type: 'pow',
algorithm: 'ethash',
ethash: {},
},
}
const forkMap: { [key: string]: { name: string; postMerge?: boolean; isTimestamp?: boolean } } = {
[Hardfork.Homestead]: { name: 'homesteadBlock' },
[Hardfork.Dao]: { name: 'daoForkBlock' },
[Hardfork.TangerineWhistle]: { name: 'eip150Block' },
[Hardfork.SpuriousDragon]: { name: 'eip155Block' },
[Hardfork.Byzantium]: { name: 'byzantiumBlock' },
[Hardfork.Constantinople]: { name: 'constantinopleBlock' },
[Hardfork.Petersburg]: { name: 'petersburgBlock' },
[Hardfork.Istanbul]: { name: 'istanbulBlock' },
[Hardfork.MuirGlacier]: { name: 'muirGlacierBlock' },
[Hardfork.Berlin]: { name: 'berlinBlock' },
[Hardfork.London]: { name: 'londonBlock' },
[Hardfork.ArrowGlacier]: { name: 'arrowGlacierBlock' },
[Hardfork.GrayGlacier]: { name: 'grayGlacierBlock' },
[Hardfork.Paris]: { name: 'mergeForkBlock', postMerge: true },
[Hardfork.MergeNetsplitBlock]: { name: 'mergeNetsplitBlock', postMerge: true },
[Hardfork.Shanghai]: { name: 'shanghaiTime', postMerge: true, isTimestamp: true },
[Hardfork.Cancun]: { name: 'cancunTime', postMerge: true, isTimestamp: true },
[Hardfork.Prague]: { name: 'pragueTime', postMerge: true, isTimestamp: true },
[Hardfork.Osaka]: { name: 'osakaTime', postMerge: true, isTimestamp: true },
[Hardfork.Verkle]: { name: 'verkleTime', postMerge: true, isTimestamp: true },
}
// forkMapRev is the map from config field name to Hardfork
const forkMapRev = Object.keys(forkMap).reduce(
(acc, elem) => {
acc[forkMap[elem].name] = elem
return acc
},
{} as { [key: string]: string },
)
params.hardforks = Object.entries(forkMapRev)
.map(([nameBlock, hardfork]) => {
const configValue = config[nameBlock as keyof typeof config]
const isTimestamp = forkMap[hardfork].isTimestamp === true
const block = isTimestamp || typeof configValue !== 'number' ? null : configValue
const timestamp = isTimestamp && typeof configValue === 'number' ? configValue : undefined
return { name: hardfork, block, timestamp }
})
.filter(({ block, timestamp }) => block !== null || timestamp !== undefined) as ConfigHardfork[]
const mergeIndex = params.hardforks.findIndex((hf) => hf.name === Hardfork.Paris)
let mergeNetsplitBlockIndex = params.hardforks.findIndex(
(hf) => hf.name === Hardfork.MergeNetsplitBlock,
)
const firstPostMergeHFIndex = params.hardforks.findIndex(
(hf) => hf.timestamp !== undefined && hf.timestamp !== null,
)
// If we are missing a mergeNetsplitBlock, we assume it is at the same block as Paris (if present)
if (mergeIndex !== -1 && mergeNetsplitBlockIndex === -1) {
params.hardforks.splice(mergeIndex + 1, 0, {
name: Hardfork.MergeNetsplitBlock,
block: params.hardforks[mergeIndex].block!,
})
mergeNetsplitBlockIndex = mergeIndex + 1
}
// or zero if not and a postmerge hardfork is set (since testnets using the geth genesis format are all currently start postmerge)
if (firstPostMergeHFIndex !== -1) {
if (mergeNetsplitBlockIndex === -1) {
params.hardforks.splice(firstPostMergeHFIndex, 0, {
name: Hardfork.MergeNetsplitBlock,
block: 0,
})
mergeNetsplitBlockIndex = firstPostMergeHFIndex
}
if (mergeIndex === -1) {
// If we don't have a Paris hardfork, add it at the mergeNetsplitBlock
params.hardforks.splice(mergeNetsplitBlockIndex, 0, {
name: Hardfork.Paris,
block: params.hardforks[mergeNetsplitBlockIndex].block!,
})
}
// Check for terminalTotalDifficultyPassed param in genesis config if no post merge hardforks are set
} else if (config.terminalTotalDifficultyPassed === true) {
if (mergeIndex === -1) {
// If we don't have a Paris hardfork, add it at end of hardfork array
params.hardforks.push({
name: Hardfork.Paris,
block: 0,
})
}
// If we don't have a MergeNetsplitBlock hardfork, add it at end of hardfork array
if (mergeNetsplitBlockIndex === -1) {
params.hardforks.push({
name: Hardfork.MergeNetsplitBlock,
block: 0,
})
mergeNetsplitBlockIndex = firstPostMergeHFIndex
}
}
// TODO: Decide if we actually need to do this since `ForkMap` specifies the order we expect things in
params.hardforks.sort(function (a: ConfigHardfork, b: ConfigHardfork) {
return (a.block ?? Infinity) - (b.block ?? Infinity)
})
params.hardforks.sort(function (a: ConfigHardfork, b: ConfigHardfork) {
// non timestamp forks come before any timestamp forks
return (a.timestamp ?? 0) - (b.timestamp ?? 0)
})
// only set the genesis timestamp forks to zero post the above sort has happened
// to get the correct sorting
for (const hf of params.hardforks) {
if (hf.timestamp === genesisTimestamp) {
hf.timestamp = 0
}
}
const latestHardfork = params.hardforks.length > 0 ? params.hardforks.slice(-1)[0] : undefined
params.hardfork = latestHardfork?.name
params.hardforks.unshift({ name: Hardfork.Chainstart, block: 0 })
return params
}
/**
* Parses a genesis object exported from Geth into parameters for Common instance
* @param gethGenesis GethGenesis object
* @param name optional chain name
* @returns parsed params
*/
export function parseGethGenesis(gethGenesis: GethGenesis, name?: string) {
try {
const required = ['config', 'difficulty', 'gasLimit', 'nonce', 'alloc']
if (required.some((field) => !(field in gethGenesis))) {
const missingField = required.filter((field) => !(field in gethGenesis))
throw EthereumJSErrorWithoutCode(
`Invalid format, expected geth genesis field "${missingField}" missing`,
)
}
// We copy the object here because it's frozen in browser and properties can't be modified
const finalGethGenesis = { ...gethGenesis }
if (name !== undefined) {
finalGethGenesis.name = name
}
return parseGethParams(finalGethGenesis)
} catch (e: any) {
throw EthereumJSErrorWithoutCode(`Error parsing parameters file: ${e.message}`)
}
}
/**
* Return the preset chain config for one of the predefined chain configurations
* @param chain the representing a network name (e.g. 'mainnet') or number representing the chain ID
* @returns a {@link ChainConfig}
*/
export const getPresetChainConfig = (chain: string | number) => {
switch (chain) {
case 'holesky':
case 17000:
return Holesky
case 'hoodi':
case 560048:
return Hoodi
case 'kaustinen6':
case 69420:
return Kaustinen6
case 'sepolia':
case 11155111:
return Sepolia
case 'mainnet':
case 1:
default:
return Mainnet
}
}