/* This file is part of web3.js. web3.js is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. web3.js is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ import { isHexPrefixed, isHexString } from 'web3-validator'; import { bytesToHex, hexToBytes, isUint8Array, numberToHex } from 'web3-utils'; import { secp256k1 } from '../tx/constants.js'; import { Hardfork } from './enums.js'; import { ToBytesInputTypes, TypeOutput, TypeOutputReturnType } from './types.js'; type ConfigHardfork = // eslint-disable-next-line @typescript-eslint/ban-types | { name: string; block: null; timestamp: number } | { name: string; block: number; timestamp?: number }; /** * Removes '0x' from a given `String` if present * @param str the string value * @returns the string without 0x prefix */ export const stripHexPrefix = (str: string): string => { if (typeof str !== 'string') throw new Error(`[stripHexPrefix] input must be type 'string', received ${typeof str}`); return isHexPrefixed(str) ? str.slice(2) : str; }; /** * 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): string { if (!nonce || nonce === '0x0') { return '0x0000000000000000'; } if (isHexPrefixed(nonce)) { return `0x${stripHexPrefix(nonce).padStart(16, '0')}`; } return `0x${nonce.padStart(16, '0')}`; } /** * Converts a `Number` into a hex `String` * @param {Number} i * @return {String} */ const intToHex = function (i: number) { if (!Number.isSafeInteger(i) || i < 0) { throw new Error(`Received an invalid integer type: ${i}`); } return `0x${i.toString(16)}`; }; /** * 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 = true) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { name, config, difficulty, mixHash, gasLimit, coinbase, baseFeePerGas, }: { name: string; config: any; difficulty: string; mixHash: string; gasLimit: string; coinbase: string; baseFeePerGas: string; } = json; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment let { extraData, timestamp, nonce }: { extraData: string; timestamp: string; nonce: string } = json; const genesisTimestamp = Number(timestamp); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { chainId }: { chainId: number } = config; // geth is not strictly putting empty fields with a 0x prefix if (extraData === '') { extraData = '0x'; } // geth may use number for timestamp if (!isHexPrefixed(timestamp)) { // eslint-disable-next-line radix timestamp = intToHex(parseInt(timestamp)); } // geth may not give us a nonce strictly formatted to an 8 byte hex string if (nonce.length !== 18) { nonce = formatNonce(nonce); } // 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 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 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, genesis: { timestamp, // eslint-disable-next-line radix gasLimit: parseInt(gasLimit), // geth gasLimit and difficulty are hex strings while ours are `number`s // eslint-disable-next-line radix difficulty: parseInt(difficulty), nonce, extraData, mixHash, coinbase, baseFeePerGas, }, hardfork: undefined as string | undefined, hardforks: [] as ConfigHardfork[], bootstrapNodes: [], consensus: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 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 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment period: config.clique.period ?? config.clique.blockperiodseconds, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment 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.ShardingForkDev]: { name: 'shardingForkTime', postMerge: true, isTimestamp: true, }, }; // forkMapRev is the map from config field name to Hardfork const forkMapRev = Object.keys(forkMap).reduce<{ [key: string]: string }>((acc, elem) => { acc[forkMap[elem].name] = elem; return acc; }, {}); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const configHardforkNames = Object.keys(config).filter( // eslint-disable-next-line no-null/no-null, @typescript-eslint/no-unsafe-member-access key => forkMapRev[key] !== undefined && config[key] !== undefined && config[key] !== null, ); params.hardforks = configHardforkNames .map(nameBlock => ({ name: forkMapRev[nameBlock], // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment block: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access forkMap[forkMapRev[nameBlock]].isTimestamp === true || // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access typeof config[nameBlock] !== 'number' ? // eslint-disable-next-line no-null/no-null null : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access config[nameBlock], // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment timestamp: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access forkMap[forkMapRev[nameBlock]].isTimestamp === true && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access typeof config[nameBlock] === 'number' ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access config[nameBlock] : undefined, })) // eslint-disable-next-line no-null/no-null .filter(fork => fork.block !== null || fork.timestamp !== undefined) as ConfigHardfork[]; params.hardforks.sort( (a: ConfigHardfork, b: ConfigHardfork) => (a.block ?? Infinity) - (b.block ?? Infinity), ); params.hardforks.sort( (a: ConfigHardfork, b: ConfigHardfork) => (a.timestamp ?? genesisTimestamp) - (b.timestamp ?? genesisTimestamp), ); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 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.Merge, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment ttd: config.terminalTotalDifficulty, // eslint-disable-next-line no-null/no-null block: null, }; // Merge hardfork has to be placed before first hardfork that is dependent on merge const postMergeIndex = params.hardforks.findIndex( // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (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 { if (['config', 'difficulty', 'gasLimit', 'alloc'].some(field => !(field in json))) { throw new Error('Invalid format, expected geth genesis fields missing'); } if (name !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-param-reassign json.name = name; } return parseGethParams(json, mergeForkIdPostMerge); } catch (e: any) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions throw new Error(`Error parsing parameters file: ${e.message}`); } } /** * Pads a `String` to have an even length * @param value * @return output */ export function padToEven(value: string): string { let a = value; if (typeof a !== 'string') { throw new Error(`[padToEven] value must be type 'string', received ${typeof a}`); } if (a.length % 2) a = `0${a}`; return a; } /** * Converts an `Number` to a `Uint8Array` * @param {Number} i * @return {Uint8Array} */ export const intToUint8Array = function (i: number) { const hex = intToHex(i); return hexToBytes(`0x${padToEven(hex.slice(2))}`); }; /** * Attempts to turn a value into a `Uint8Array`. * Inputs supported: `Uint8Array` `String` (hex-prefixed), `Number`, null/undefined, `BigInt` and other objects * with a `toArray()` or `toUint8Array()` method. * @param v the value */ export const toUint8Array = function (v: ToBytesInputTypes): Uint8Array { // eslint-disable-next-line no-null/no-null if (v === null || v === undefined) { return new Uint8Array(); } if (v instanceof Uint8Array) { return v; } if (v?.constructor?.name === 'Uint8Array') { return Uint8Array.from(v as unknown as Uint8Array); } if (Array.isArray(v)) { return Uint8Array.from(v); } if (typeof v === 'string') { if (!isHexString(v)) { throw new Error( `Cannot convert string to Uint8Array. only supports 0x-prefixed hex strings and this string was given: ${v}`, ); } return hexToBytes(padToEven(stripHexPrefix(v))); } if (typeof v === 'number') { return toUint8Array(numberToHex(v)); } if (typeof v === 'bigint') { if (v < BigInt(0)) { throw new Error(`Cannot convert negative bigint to Uint8Array. Given: ${v}`); } let n = v.toString(16); if (n.length % 2) n = `0${n}`; return toUint8Array(`0x${n}`); } if (v.toArray) { // converts a BN to a Uint8Array return Uint8Array.from(v.toArray()); } throw new Error('invalid type'); }; /** * Converts a {@link Uint8Array} to a {@link bigint} */ export function uint8ArrayToBigInt(buf: Uint8Array) { const hex = bytesToHex(buf); if (hex === '0x') { return BigInt(0); } return BigInt(hex); } /** * Converts a {@link bigint} to a {@link Uint8Array} */ export function bigIntToUint8Array(num: bigint) { return toUint8Array(`0x${num.toString(16)}`); } /** * Returns a Uint8Array filled with 0s. * @param bytes the number of bytes the Uint8Array should be */ export const zeros = function (bytes: number): Uint8Array { return new Uint8Array(bytes).fill(0); }; /** * Pads a `Uint8Array` with zeros till it has `length` bytes. * Truncates the beginning or end of input if its length exceeds `length`. * @param msg the value to pad (Uint8Array) * @param length the number of bytes the output should be * @param right whether to start padding form the left or right * @return (Uint8Array) */ const setLength = function (msg: Uint8Array, length: number, right: boolean) { const buf = zeros(length); if (right) { if (msg.length < length) { buf.set(msg); return buf; } return msg.subarray(0, length); } if (msg.length < length) { buf.set(msg, length - msg.length); return buf; } return msg.subarray(-length); }; /** * Throws if input is not a Uint8Array * @param {Uint8Array} input value to check */ export function assertIsUint8Array(input: unknown): asserts input is Uint8Array { if (!isUint8Array(input)) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions const msg = `This method only supports Uint8Array but input was: ${input}`; throw new Error(msg); } } /** * Left Pads a `Uint8Array` with leading zeros till it has `length` bytes. * Or it truncates the beginning if it exceeds. * @param msg the value to pad (Uint8Array) * @param length the number of bytes the output should be * @return (Uint8Array) */ export const setLengthLeft = function (msg: Uint8Array, length: number) { assertIsUint8Array(msg); return setLength(msg, length, false); }; /** * Trims leading zeros from a `Uint8Array`, `String` or `Number[]`. * @param a (Uint8Array|Array|String) * @return (Uint8Array|Array|String) */ export function stripZeros(a: T): T { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment let first = a[0]; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call while (a.length > 0 && first.toString() === '0') { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, prefer-destructuring, @typescript-eslint/no-unsafe-call, no-param-reassign a = a.slice(1) as T; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, prefer-destructuring, @typescript-eslint/no-unsafe-member-access first = a[0]; } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return a; } /** * Trims leading zeros from a `Uint8Array`. * @param a (Uint8Array) * @return (Uint8Array) */ export const unpadUint8Array = function (a: Uint8Array): Uint8Array { assertIsUint8Array(a); return stripZeros(a); }; /** * Converts a {@link bigint} to a `0x` prefixed hex string */ export const bigIntToHex = (num: bigint) => `0x${num.toString(16)}`; /** * Convert value from bigint to an unpadded Uint8Array * (useful for RLP transport) * @param value value to convert */ export function bigIntToUnpaddedUint8Array(value: bigint): Uint8Array { return unpadUint8Array(bigIntToUint8Array(value)); } function calculateSigRecovery(v: bigint, chainId?: bigint): bigint { if (v === BigInt(0) || v === BigInt(1)) return v; if (chainId === undefined) { return v - BigInt(27); } return v - (chainId * BigInt(2) + BigInt(35)); } function isValidSigRecovery(recovery: bigint): boolean { return recovery === BigInt(0) || recovery === BigInt(1); } /** * ECDSA public key recovery from signature. * NOTE: Accepts `v === 0 | v === 1` for EIP1559 transactions * @returns Recovered public key */ export const ecrecover = function ( msgHash: Uint8Array, v: bigint, r: Uint8Array, s: Uint8Array, chainId?: bigint, ): Uint8Array { const recovery = calculateSigRecovery(v, chainId); if (!isValidSigRecovery(recovery)) { throw new Error('Invalid signature v value'); } const senderPubKey = new secp256k1.Signature(uint8ArrayToBigInt(r), uint8ArrayToBigInt(s)) .addRecoveryBit(Number(recovery)) .recoverPublicKey(msgHash) .toRawBytes(false); return senderPubKey.slice(1); }; /** * Convert an input to a specified type. * Input of null/undefined returns null/undefined regardless of the output type. * @param input value to convert * @param outputType type to output */ // eslint-disable-next-line @typescript-eslint/ban-types export function toType(input: null, outputType: T): null; export function toType(input: undefined, outputType: T): undefined; export function toType( input: ToBytesInputTypes, outputType: T, ): TypeOutputReturnType[T]; export function toType( input: ToBytesInputTypes, outputType: T, // eslint-disable-next-line @typescript-eslint/ban-types ): TypeOutputReturnType[T] | undefined | null { // eslint-disable-next-line no-null/no-null if (input === null) { // eslint-disable-next-line no-null/no-null return null; } if (input === undefined) { return undefined; } if (typeof input === 'string' && !isHexString(input)) { throw new Error(`A string must be provided with a 0x-prefix, given: ${input}`); } else if (typeof input === 'number' && !Number.isSafeInteger(input)) { throw new Error( 'The provided number is greater than MAX_SAFE_INTEGER (please use an alternative input type)', ); } const output = toUint8Array(input); switch (outputType) { case TypeOutput.Uint8Array: return output as TypeOutputReturnType[T]; case TypeOutput.BigInt: return uint8ArrayToBigInt(output) as TypeOutputReturnType[T]; case TypeOutput.Number: { const bigInt = uint8ArrayToBigInt(output); if (bigInt > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error( 'The provided number is greater than MAX_SAFE_INTEGER (please use an alternative output type)', ); } return Number(bigInt) as TypeOutputReturnType[T]; } case TypeOutput.PrefixedHexString: return bytesToHex(output) as TypeOutputReturnType[T]; default: throw new Error('unknown outputType'); } }