Skip to content

Commit

Permalink
Use noble bytes conversion utilities internally (#3698)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
acolytec3 authored Sep 24, 2024
1 parent 7e9ac29 commit 3a7e07f
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 74 deletions.
1 change: 1 addition & 0 deletions config/vitest.config.browser.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
},
})

Expand Down
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/client/test/net/protocol/ethprotocol.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/client/test/net/protocol/lesprotocol.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand Down
1 change: 0 additions & 1 deletion packages/evm/vitest.config.browser.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] }
})
)
38 changes: 28 additions & 10 deletions packages/rlp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion packages/util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
77 changes: 19 additions & 58 deletions packages/util/src/bytes.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}

/******************************************/

/**
Expand All @@ -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
}

/**
Expand Down
17 changes: 17 additions & 0 deletions packages/util/test/bench/bytes.bench.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 3a7e07f

Please sign in to comment.