diff --git a/config/vitest.config.browser.mts b/config/vitest.config.browser.mts index a8fb4f93e0..5797c2828f 100644 --- a/config/vitest.config.browser.mts +++ b/config/vitest.config.browser.mts @@ -22,6 +22,7 @@ const config = defineConfig({ ], optimizeDeps: { exclude: ['kzg-wasm'], + include: ['vite-plugin-node-polyfills/shims/buffer', 'vite-plugin-node-polyfills/shims/global', 'vite-plugin-node-polyfills/shims/process'] }, }) diff --git a/package-lock.json b/package-lock.json index 6e1d8b6e10..5e690fc04a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15475,7 +15475,8 @@ "devDependencies": { "@paulmillr/trusted-setups": "^0.1.2", "kzg-wasm": "^0.5.0", - "micro-eth-signer": "^0.11.0" + "micro-eth-signer": "^0.11.0", + "verkle-cryptography-wasm": "^0.4.8" }, "engines": { "node": ">=18" diff --git a/packages/client/test/net/protocol/ethprotocol.spec.ts b/packages/client/test/net/protocol/ethprotocol.spec.ts index d78ffc865b..dbdc1ec13a 100644 --- a/packages/client/test/net/protocol/ethprotocol.spec.ts +++ b/packages/client/test/net/protocol/ethprotocol.spec.ts @@ -70,7 +70,7 @@ describe('[EthProtocol]', () => { 'encode status', ) const status = p.decodeStatus({ - chainId: [0x01], + chainId: Uint8Array.from([0x01]), td: hexToBytes('0x64'), bestHash: '0xaa', genesisHash: '0xbb', diff --git a/packages/client/test/net/protocol/lesprotocol.spec.ts b/packages/client/test/net/protocol/lesprotocol.spec.ts index e142c9cdf3..1f896c717a 100644 --- a/packages/client/test/net/protocol/lesprotocol.spec.ts +++ b/packages/client/test/net/protocol/lesprotocol.spec.ts @@ -84,7 +84,7 @@ describe('[LesProtocol]', () => { bytesToHex(status['flowControl/MRC'][0][2]) === '0x0a', 'encode status', ) - status = { ...status, chainId: [0x01] } + status = { ...status, chainId: Uint8Array.from([0x01]) } status = p.decodeStatus(status) assert.ok( status.chainId === BigInt(1) && diff --git a/packages/evm/vitest.config.browser.mts b/packages/evm/vitest.config.browser.mts index aa6f3474d9..d96caec834 100644 --- a/packages/evm/vitest.config.browser.mts +++ b/packages/evm/vitest.config.browser.mts @@ -12,6 +12,5 @@ export default mergeConfig( 'test/precompiles/eip-2537-bls.spec.ts', ] }, - optimizeDeps: { entries: ['vite-plugin-node-polyfills/shims/buffer', 'vite-plugin-node-polyfills/shims/global', 'vite-plugin-node-polyfills/shims/process'] } }) ) \ No newline at end of file diff --git a/packages/rlp/src/index.ts b/packages/rlp/src/index.ts index 175b6f89b4..b7d9546425 100644 --- a/packages/rlp/src/index.ts +++ b/packages/rlp/src/index.ts @@ -202,16 +202,34 @@ function parseHexByte(hexByte: string): number { return byte } -// Caching slows it down 2-3x -function hexToBytes(hex: string): Uint8Array { - if (typeof hex !== 'string') { - throw new TypeError('hexToBytes: expected string, got ' + typeof hex) - } - if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex') - const array = new Uint8Array(hex.length / 2) - for (let i = 0; i < array.length; i++) { - const j = i * 2 - array[i] = parseHexByte(hex.slice(j, j + 2)) +// Borrowed from @noble/curves to avoid dependency +// Original code here - https://github.com/paulmillr/noble-curves/blob/d0a8d2134c5737d9d0aa81be13581cd416ebdeb4/src/abstract/utils.ts#L63-L91 +const asciis = { _0: 48, _9: 57, _A: 65, _F: 70, _a: 97, _f: 102 } as const +function asciiToBase16(char: number): number | undefined { + if (char >= asciis._0 && char <= asciis._9) return char - asciis._0 + if (char >= asciis._A && char <= asciis._F) return char - (asciis._A - 10) + if (char >= asciis._a && char <= asciis._f) return char - (asciis._a - 10) + return +} + +/** + * @example hexToBytes('0xcafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23]) + */ +export function hexToBytes(hex: string): Uint8Array { + if (hex.slice(0, 2) === '0x') hex = hex.slice(0, 2) + if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex) + const hl = hex.length + const al = hl / 2 + if (hl % 2) throw new Error('padded hex string expected, got unpadded hex of length ' + hl) + const array = new Uint8Array(al) + for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) { + const n1 = asciiToBase16(hex.charCodeAt(hi)) + const n2 = asciiToBase16(hex.charCodeAt(hi + 1)) + if (n1 === undefined || n2 === undefined) { + const char = hex[hi] + hex[hi + 1] + throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi) + } + array[ai] = n1 * 16 + n2 } return array } diff --git a/packages/util/package.json b/packages/util/package.json index d0417adbc1..6bafb0191c 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -98,7 +98,8 @@ "devDependencies": { "@paulmillr/trusted-setups": "^0.1.2", "kzg-wasm": "^0.5.0", - "micro-eth-signer": "^0.11.0" + "micro-eth-signer": "^0.11.0", + "verkle-cryptography-wasm": "^0.4.8" }, "engines": { "node": ">=18" diff --git a/packages/util/src/bytes.ts b/packages/util/src/bytes.ts index ba3452055e..067ce518dc 100644 --- a/packages/util/src/bytes.ts +++ b/packages/util/src/bytes.ts @@ -1,6 +1,9 @@ import { getRandomBytesSync } from 'ethereum-cryptography/random.js' // eslint-disable-next-line no-restricted-imports -import { bytesToHex as _bytesToUnprefixedHex } from 'ethereum-cryptography/utils.js' +import { + bytesToHex as _bytesToUnprefixedHex, + hexToBytes as nobleH2B, +} from 'ethereum-cryptography/utils.js' import { assertIsArray, assertIsBytes, assertIsHexString } from './helpers.js' import { isHexString, padToEven, stripHexPrefix } from './internal.js' @@ -14,48 +17,26 @@ const BIGINT_0 = BigInt(0) */ export const bytesToUnprefixedHex = _bytesToUnprefixedHex -// hexToBytes cache -const hexToBytesMapFirstKey: { [key: string]: number } = {} -const hexToBytesMapSecondKey: { [key: string]: number } = {} - -for (let i = 0; i < 16; i++) { - const vSecondKey = i - const vFirstKey = i * 16 - const key = i.toString(16).toLowerCase() - hexToBytesMapSecondKey[key] = vSecondKey - hexToBytesMapSecondKey[key.toUpperCase()] = vSecondKey - hexToBytesMapFirstKey[key] = vFirstKey - hexToBytesMapFirstKey[key.toUpperCase()] = vFirstKey -} - /** - * @deprecated + * Converts a {@link PrefixedHexString} to a {@link Uint8Array} + * @param {PrefixedHexString} hex The 0x-prefixed hex string to convert + * @returns {Uint8Array} The converted bytes + * @throws If the input is not a valid 0x-prefixed hex string */ -export const unprefixedHexToBytes = (inp: string) => { - if (inp.slice(0, 2) === '0x') { - throw new Error('hex string is prefixed with 0x, should be unprefixed') - } else { - inp = padToEven(inp) - const byteLen = inp.length - const bytes = new Uint8Array(byteLen / 2) - for (let i = 0; i < byteLen; i += 2) { - bytes[i / 2] = hexToBytesMapFirstKey[inp[i]] + hexToBytesMapSecondKey[inp[i + 1]] - } - return bytes - } +export const hexToBytes = (hex: string) => { + if (!hex.startsWith('0x')) throw new Error('input string must be 0x prefixed') + return nobleH2B(padToEven(stripHexPrefix(hex))) } -/**************** Borrowed from @chainsafe/ssz */ -// Caching this info costs about ~1000 bytes and speeds up toHexString() by x6 -const hexByByte = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0')) +export const unprefixedHexToBytes = (hex: string) => { + if (hex.startsWith('0x')) throw new Error('input string cannot be 0x prefixed') + return nobleH2B(padToEven(hex)) +} export const bytesToHex = (bytes: Uint8Array): PrefixedHexString => { - let hex: PrefixedHexString = `0x` - if (bytes === undefined || bytes.length === 0) return hex - for (const byte of bytes) { - hex = `${hex}${hexByByte[byte]}` - } - return hex + if (bytes === undefined || bytes.length === 0) return '0x' + const unprefixedHex = bytesToUnprefixedHex(bytes) + return ('0x' + unprefixedHex) as PrefixedHexString } // BigInt cache for the numbers 0 - 256*256-1 (two-byte bytes) @@ -99,26 +80,6 @@ export const bytesToInt = (bytes: Uint8Array): number => { return res } -/** - * Converts a {@link PrefixedHexString} to a {@link Uint8Array} - * @param {PrefixedHexString} hex The 0x-prefixed hex string to convert - * @returns {Uint8Array} The converted bytes - * @throws If the input is not a valid 0x-prefixed hex string - */ -export const hexToBytes = (hex: PrefixedHexString): Uint8Array => { - if (typeof hex !== 'string') { - throw new Error(`hex argument type ${typeof hex} must be of type string`) - } - - if (!/^0x[0-9a-fA-F]*$/.test(hex)) { - throw new Error(`Input must be a 0x-prefixed hexadecimal string, got ${hex}`) - } - - const unprefixedHex = hex.slice(2) - - return unprefixedHexToBytes(unprefixedHex) -} - /******************************************/ /** @@ -130,7 +91,7 @@ export const intToHex = (i: number): PrefixedHexString => { if (!Number.isSafeInteger(i) || i < 0) { throw new Error(`Received an invalid integer type: ${i}`) } - return `0x${i.toString(16)}` + return ('0x' + i.toString(16)) as PrefixedHexString } /** diff --git a/packages/util/test/bench/bytes.bench.ts b/packages/util/test/bench/bytes.bench.ts new file mode 100644 index 0000000000..5503e81351 --- /dev/null +++ b/packages/util/test/bench/bytes.bench.ts @@ -0,0 +1,17 @@ +import { bench, describe } from 'vitest' + +import { bytesToHex, hexToBytes } from '../../src/bytes.js' + +// Simple benchmarks for our bytes conversion utility +describe('hexToBytes', () => { + bench('hexToBytes', () => { + hexToBytes('0xcafe1234') + }) +}) + +describe('bytesToHex', () => { + const bytes = new Uint8Array(4).fill(4) + bench('bytesToHex', () => { + bytesToHex(bytes) + }) +}) diff --git a/packages/util/test/kzg.bench.ts b/packages/util/test/bench/kzg.bench.ts similarity index 86% rename from packages/util/test/kzg.bench.ts rename to packages/util/test/bench/kzg.bench.ts index 47099df07c..4b5e3a9a5b 100644 --- a/packages/util/test/kzg.bench.ts +++ b/packages/util/test/bench/kzg.bench.ts @@ -2,8 +2,11 @@ import { getBlobs } from '@ethereumjs/util' import { loadKZG } from 'kzg-wasm' import { bench, describe } from 'vitest' -import { jsKZG } from './kzg.spec.js' +import { jsKZG } from '../kzg.spec.js' +/** + * These benchmarks compare performance of various KZG related functions for our two supported backends + */ describe('benchmarks', async () => { const kzg = await loadKZG() const blob = getBlobs('hello')[0]