239 lines
8.6 KiB
TypeScript
239 lines
8.6 KiB
TypeScript
import { intToHex, isHexString, stripHexPrefix } from '@ethereumjs/util'
|
|
|
|
import { Hardfork } from './enums.js'
|
|
|
|
import type { PrefixedHexString } from '@ethereumjs/util'
|
|
|
|
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 json object representing the Geth genesis file
|
|
* @param optional mergeForkIdPostMerge which clarifies the placement of MergeForkIdTransition
|
|
* hardfork, which by default is post merge as with the merged eth networks but could also come
|
|
* before merge like in kiln genesis
|
|
* @returns genesis parameters in a `CommonOpts` compliant object
|
|
*/
|
|
function parseGethParams(json: any, mergeForkIdPostMerge: boolean = true) {
|
|
const {
|
|
name,
|
|
config,
|
|
difficulty,
|
|
mixHash,
|
|
gasLimit,
|
|
coinbase,
|
|
baseFeePerGas,
|
|
excessBlobGas,
|
|
extraData: unparsedExtraData,
|
|
nonce: unparsedNonce,
|
|
timestamp: unparsedTimestamp,
|
|
}: {
|
|
name: string
|
|
config: any
|
|
difficulty: PrefixedHexString
|
|
mixHash: PrefixedHexString
|
|
gasLimit: PrefixedHexString
|
|
coinbase: PrefixedHexString
|
|
baseFeePerGas: PrefixedHexString
|
|
excessBlobGas: PrefixedHexString
|
|
extraData: string
|
|
nonce: string
|
|
timestamp: string
|
|
} = json
|
|
const genesisTimestamp = Number(unparsedTimestamp)
|
|
const {
|
|
chainId,
|
|
depositContractAddress,
|
|
}: { chainId: number; depositContractAddress: PrefixedHexString } = config
|
|
|
|
// geth is not strictly putting empty fields with a 0x prefix
|
|
const extraData: PrefixedHexString =
|
|
unparsedExtraData === '' ? '0x' : (unparsedExtraData as PrefixedHexString)
|
|
|
|
// 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: PrefixedHexString =
|
|
unparsedNonce.length !== 18 ? formatNonce(unparsedNonce) : (unparsedNonce as PrefixedHexString)
|
|
|
|
// 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 new Error(
|
|
'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'
|
|
)
|
|
}
|
|
|
|
const params = {
|
|
name,
|
|
chainId,
|
|
networkId: chainId,
|
|
depositContractAddress,
|
|
genesis: {
|
|
timestamp,
|
|
gasLimit,
|
|
difficulty,
|
|
nonce,
|
|
extraData,
|
|
mixHash,
|
|
coinbase,
|
|
baseFeePerGas,
|
|
excessBlobGas,
|
|
},
|
|
hardfork: undefined as string | undefined,
|
|
hardforks: [] as ConfigHardfork[],
|
|
bootstrapNodes: [],
|
|
consensus:
|
|
config.clique !== undefined
|
|
? {
|
|
type: 'poa',
|
|
algorithm: 'clique',
|
|
clique: {
|
|
// The recent geth genesis seems to be using blockperiodseconds
|
|
// and epochlength for clique specification
|
|
// see: https://hackmd.io/PqZgMpnkSWCWv5joJoFymQ
|
|
period: config.clique.period ?? config.clique.blockperiodseconds,
|
|
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.MergeForkIdTransition]: { name: 'mergeForkBlock', postMerge: mergeForkIdPostMerge },
|
|
[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 },
|
|
}
|
|
|
|
// 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 })
|
|
const configHardforkNames = Object.keys(config).filter(
|
|
(key) => forkMapRev[key] !== undefined && config[key] !== undefined && config[key] !== null
|
|
)
|
|
|
|
params.hardforks = configHardforkNames
|
|
.map((nameBlock) => ({
|
|
name: forkMapRev[nameBlock],
|
|
block:
|
|
forkMap[forkMapRev[nameBlock]].isTimestamp === true || typeof config[nameBlock] !== 'number'
|
|
? null
|
|
: config[nameBlock],
|
|
timestamp:
|
|
forkMap[forkMapRev[nameBlock]].isTimestamp === true && typeof config[nameBlock] === 'number'
|
|
? config[nameBlock]
|
|
: undefined,
|
|
}))
|
|
.filter((fork) => fork.block !== null || fork.timestamp !== undefined) as ConfigHardfork[]
|
|
|
|
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 happended
|
|
// to get the correct sorting
|
|
for (const hf of params.hardforks) {
|
|
if (hf.timestamp === genesisTimestamp) {
|
|
hf.timestamp = 0
|
|
}
|
|
}
|
|
|
|
if (config.terminalTotalDifficulty !== undefined) {
|
|
// Following points need to be considered for placement of merge hf
|
|
// - Merge hardfork can't be placed at genesis
|
|
// - Place merge hf before any hardforks that require CL participation for e.g. withdrawals
|
|
// - Merge hardfork has to be placed just after genesis if any of the genesis hardforks make CL
|
|
// necessary for e.g. withdrawals
|
|
const mergeConfig = {
|
|
name: Hardfork.Paris,
|
|
ttd: config.terminalTotalDifficulty,
|
|
block: null,
|
|
}
|
|
|
|
// Merge hardfork has to be placed before first hardfork that is dependent on merge
|
|
const postMergeIndex = params.hardforks.findIndex(
|
|
(hf: any) => forkMap[hf.name]?.postMerge === true
|
|
)
|
|
if (postMergeIndex !== -1) {
|
|
params.hardforks.splice(postMergeIndex, 0, mergeConfig as unknown as ConfigHardfork)
|
|
} else {
|
|
params.hardforks.push(mergeConfig as unknown as ConfigHardfork)
|
|
}
|
|
}
|
|
|
|
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.json exported from Geth into parameters for Common instance
|
|
* @param json representing the Geth genesis file
|
|
* @param name optional chain name
|
|
* @returns parsed params
|
|
*/
|
|
export function parseGethGenesis(json: any, name?: string, mergeForkIdPostMerge?: boolean) {
|
|
try {
|
|
const required = ['config', 'difficulty', 'gasLimit', 'nonce', 'alloc']
|
|
if (required.some((field) => !(field in json))) {
|
|
const missingField = required.filter((field) => !(field in json))
|
|
throw new Error(`Invalid format, expected geth genesis field "${missingField}" missing`)
|
|
}
|
|
|
|
// We copy the JSON object here because it's frozen in browser and properties can't be modified
|
|
const finalJson = { ...json }
|
|
|
|
if (name !== undefined) {
|
|
finalJson.name = name
|
|
}
|
|
return parseGethParams(finalJson, mergeForkIdPostMerge)
|
|
} catch (e: any) {
|
|
throw new Error(`Error parsing parameters file: ${e.message}`)
|
|
}
|
|
}
|