Skip to content

Commit 3a7e07f

Browse files
authored
Use noble bytes conversion utilities internally (#3698)
* use noble bytes converters internally * clean up benchmarks and comments * Ensure bytes are bytes * revert unnecessary bytes conversion * Add devdep for verkle-crypto * specify browser deps to optimize * specify browser deps to optimize * Remove commented code * optimize deps everywhere * Add bytes benchmark
1 parent 7e9ac29 commit 3a7e07f

File tree

10 files changed

+75
-74
lines changed

10 files changed

+75
-74
lines changed

config/vitest.config.browser.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const config = defineConfig({
2222
],
2323
optimizeDeps: {
2424
exclude: ['kzg-wasm'],
25+
include: ['vite-plugin-node-polyfills/shims/buffer', 'vite-plugin-node-polyfills/shims/global', 'vite-plugin-node-polyfills/shims/process']
2526
},
2627
})
2728

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/client/test/net/protocol/ethprotocol.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('[EthProtocol]', () => {
7070
'encode status',
7171
)
7272
const status = p.decodeStatus({
73-
chainId: [0x01],
73+
chainId: Uint8Array.from([0x01]),
7474
td: hexToBytes('0x64'),
7575
bestHash: '0xaa',
7676
genesisHash: '0xbb',

packages/client/test/net/protocol/lesprotocol.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('[LesProtocol]', () => {
8484
bytesToHex(status['flowControl/MRC'][0][2]) === '0x0a',
8585
'encode status',
8686
)
87-
status = { ...status, chainId: [0x01] }
87+
status = { ...status, chainId: Uint8Array.from([0x01]) }
8888
status = p.decodeStatus(status)
8989
assert.ok(
9090
status.chainId === BigInt(1) &&

packages/evm/vitest.config.browser.mts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,5 @@ export default mergeConfig(
1212
'test/precompiles/eip-2537-bls.spec.ts',
1313
]
1414
},
15-
optimizeDeps: { entries: ['vite-plugin-node-polyfills/shims/buffer', 'vite-plugin-node-polyfills/shims/global', 'vite-plugin-node-polyfills/shims/process'] }
1615
})
1716
)

packages/rlp/src/index.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,34 @@ function parseHexByte(hexByte: string): number {
202202
return byte
203203
}
204204

205-
// Caching slows it down 2-3x
206-
function hexToBytes(hex: string): Uint8Array {
207-
if (typeof hex !== 'string') {
208-
throw new TypeError('hexToBytes: expected string, got ' + typeof hex)
209-
}
210-
if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex')
211-
const array = new Uint8Array(hex.length / 2)
212-
for (let i = 0; i < array.length; i++) {
213-
const j = i * 2
214-
array[i] = parseHexByte(hex.slice(j, j + 2))
205+
// Borrowed from @noble/curves to avoid dependency
206+
// Original code here - https://github.com/paulmillr/noble-curves/blob/d0a8d2134c5737d9d0aa81be13581cd416ebdeb4/src/abstract/utils.ts#L63-L91
207+
const asciis = { _0: 48, _9: 57, _A: 65, _F: 70, _a: 97, _f: 102 } as const
208+
function asciiToBase16(char: number): number | undefined {
209+
if (char >= asciis._0 && char <= asciis._9) return char - asciis._0
210+
if (char >= asciis._A && char <= asciis._F) return char - (asciis._A - 10)
211+
if (char >= asciis._a && char <= asciis._f) return char - (asciis._a - 10)
212+
return
213+
}
214+
215+
/**
216+
* @example hexToBytes('0xcafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23])
217+
*/
218+
export function hexToBytes(hex: string): Uint8Array {
219+
if (hex.slice(0, 2) === '0x') hex = hex.slice(0, 2)
220+
if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex)
221+
const hl = hex.length
222+
const al = hl / 2
223+
if (hl % 2) throw new Error('padded hex string expected, got unpadded hex of length ' + hl)
224+
const array = new Uint8Array(al)
225+
for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
226+
const n1 = asciiToBase16(hex.charCodeAt(hi))
227+
const n2 = asciiToBase16(hex.charCodeAt(hi + 1))
228+
if (n1 === undefined || n2 === undefined) {
229+
const char = hex[hi] + hex[hi + 1]
230+
throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi)
231+
}
232+
array[ai] = n1 * 16 + n2
215233
}
216234
return array
217235
}

packages/util/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@
9898
"devDependencies": {
9999
"@paulmillr/trusted-setups": "^0.1.2",
100100
"kzg-wasm": "^0.5.0",
101-
"micro-eth-signer": "^0.11.0"
101+
"micro-eth-signer": "^0.11.0",
102+
"verkle-cryptography-wasm": "^0.4.8"
102103
},
103104
"engines": {
104105
"node": ">=18"

packages/util/src/bytes.ts

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { getRandomBytesSync } from 'ethereum-cryptography/random.js'
22
// eslint-disable-next-line no-restricted-imports
3-
import { bytesToHex as _bytesToUnprefixedHex } from 'ethereum-cryptography/utils.js'
3+
import {
4+
bytesToHex as _bytesToUnprefixedHex,
5+
hexToBytes as nobleH2B,
6+
} from 'ethereum-cryptography/utils.js'
47

58
import { assertIsArray, assertIsBytes, assertIsHexString } from './helpers.js'
69
import { isHexString, padToEven, stripHexPrefix } from './internal.js'
@@ -14,48 +17,26 @@ const BIGINT_0 = BigInt(0)
1417
*/
1518
export const bytesToUnprefixedHex = _bytesToUnprefixedHex
1619

17-
// hexToBytes cache
18-
const hexToBytesMapFirstKey: { [key: string]: number } = {}
19-
const hexToBytesMapSecondKey: { [key: string]: number } = {}
20-
21-
for (let i = 0; i < 16; i++) {
22-
const vSecondKey = i
23-
const vFirstKey = i * 16
24-
const key = i.toString(16).toLowerCase()
25-
hexToBytesMapSecondKey[key] = vSecondKey
26-
hexToBytesMapSecondKey[key.toUpperCase()] = vSecondKey
27-
hexToBytesMapFirstKey[key] = vFirstKey
28-
hexToBytesMapFirstKey[key.toUpperCase()] = vFirstKey
29-
}
30-
3120
/**
32-
* @deprecated
21+
* Converts a {@link PrefixedHexString} to a {@link Uint8Array}
22+
* @param {PrefixedHexString} hex The 0x-prefixed hex string to convert
23+
* @returns {Uint8Array} The converted bytes
24+
* @throws If the input is not a valid 0x-prefixed hex string
3325
*/
34-
export const unprefixedHexToBytes = (inp: string) => {
35-
if (inp.slice(0, 2) === '0x') {
36-
throw new Error('hex string is prefixed with 0x, should be unprefixed')
37-
} else {
38-
inp = padToEven(inp)
39-
const byteLen = inp.length
40-
const bytes = new Uint8Array(byteLen / 2)
41-
for (let i = 0; i < byteLen; i += 2) {
42-
bytes[i / 2] = hexToBytesMapFirstKey[inp[i]] + hexToBytesMapSecondKey[inp[i + 1]]
43-
}
44-
return bytes
45-
}
26+
export const hexToBytes = (hex: string) => {
27+
if (!hex.startsWith('0x')) throw new Error('input string must be 0x prefixed')
28+
return nobleH2B(padToEven(stripHexPrefix(hex)))
4629
}
4730

48-
/**************** Borrowed from @chainsafe/ssz */
49-
// Caching this info costs about ~1000 bytes and speeds up toHexString() by x6
50-
const hexByByte = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'))
31+
export const unprefixedHexToBytes = (hex: string) => {
32+
if (hex.startsWith('0x')) throw new Error('input string cannot be 0x prefixed')
33+
return nobleH2B(padToEven(hex))
34+
}
5135

5236
export const bytesToHex = (bytes: Uint8Array): PrefixedHexString => {
53-
let hex: PrefixedHexString = `0x`
54-
if (bytes === undefined || bytes.length === 0) return hex
55-
for (const byte of bytes) {
56-
hex = `${hex}${hexByByte[byte]}`
57-
}
58-
return hex
37+
if (bytes === undefined || bytes.length === 0) return '0x'
38+
const unprefixedHex = bytesToUnprefixedHex(bytes)
39+
return ('0x' + unprefixedHex) as PrefixedHexString
5940
}
6041

6142
// BigInt cache for the numbers 0 - 256*256-1 (two-byte bytes)
@@ -99,26 +80,6 @@ export const bytesToInt = (bytes: Uint8Array): number => {
9980
return res
10081
}
10182

102-
/**
103-
* Converts a {@link PrefixedHexString} to a {@link Uint8Array}
104-
* @param {PrefixedHexString} hex The 0x-prefixed hex string to convert
105-
* @returns {Uint8Array} The converted bytes
106-
* @throws If the input is not a valid 0x-prefixed hex string
107-
*/
108-
export const hexToBytes = (hex: PrefixedHexString): Uint8Array => {
109-
if (typeof hex !== 'string') {
110-
throw new Error(`hex argument type ${typeof hex} must be of type string`)
111-
}
112-
113-
if (!/^0x[0-9a-fA-F]*$/.test(hex)) {
114-
throw new Error(`Input must be a 0x-prefixed hexadecimal string, got ${hex}`)
115-
}
116-
117-
const unprefixedHex = hex.slice(2)
118-
119-
return unprefixedHexToBytes(unprefixedHex)
120-
}
121-
12283
/******************************************/
12384

12485
/**
@@ -130,7 +91,7 @@ export const intToHex = (i: number): PrefixedHexString => {
13091
if (!Number.isSafeInteger(i) || i < 0) {
13192
throw new Error(`Received an invalid integer type: ${i}`)
13293
}
133-
return `0x${i.toString(16)}`
94+
return ('0x' + i.toString(16)) as PrefixedHexString
13495
}
13596

13697
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { bench, describe } from 'vitest'
2+
3+
import { bytesToHex, hexToBytes } from '../../src/bytes.js'
4+
5+
// Simple benchmarks for our bytes conversion utility
6+
describe('hexToBytes', () => {
7+
bench('hexToBytes', () => {
8+
hexToBytes('0xcafe1234')
9+
})
10+
})
11+
12+
describe('bytesToHex', () => {
13+
const bytes = new Uint8Array(4).fill(4)
14+
bench('bytesToHex', () => {
15+
bytesToHex(bytes)
16+
})
17+
})

packages/util/test/kzg.bench.ts renamed to packages/util/test/bench/kzg.bench.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { getBlobs } from '@ethereumjs/util'
22
import { loadKZG } from 'kzg-wasm'
33
import { bench, describe } from 'vitest'
44

5-
import { jsKZG } from './kzg.spec.js'
5+
import { jsKZG } from '../kzg.spec.js'
66

7+
/**
8+
* These benchmarks compare performance of various KZG related functions for our two supported backends
9+
*/
710
describe('benchmarks', async () => {
811
const kzg = await loadKZG()
912
const blob = getBlobs('hello')[0]

0 commit comments

Comments
 (0)