diff --git a/packages/utils/src/bytes/browser.ts b/packages/utils/src/bytes/browser.ts index f610e2912c..27abb185e4 100644 --- a/packages/utils/src/bytes/browser.ts +++ b/packages/utils/src/bytes/browser.ts @@ -60,12 +60,29 @@ export function fromHex(hex: string): Uint8Array { const byteLen = hex.length / 2; const bytes = new Uint8Array(byteLen); for (let i = 0; i < byteLen; i++) { - const byte = parseInt(hex.slice(i * 2, (i + 1) * 2), 16); - bytes[i] = byte; + const byte2i = charCodeToByte(hex.charCodeAt(i * 2)); + const byte2i1 = charCodeToByte(hex.charCodeAt(i * 2 + 1)); + bytes[i] = (byte2i << 4) | byte2i1; } return bytes; } +export function fromHexInto(hex: string, buffer: Uint8Array): void { + if (hex.startsWith("0x")) { + hex = hex.slice(2); + } + + if (hex.length !== buffer.length * 2) { + throw new Error(`hex string length ${hex.length} must be exactly double the buffer length ${buffer.length}`); + } + + for (let i = 0; i < buffer.length; i++) { + const byte2i = charCodeToByte(hex.charCodeAt(i * 2)); + const byte2i1 = charCodeToByte(hex.charCodeAt(i * 2 + 1)); + buffer[i] = (byte2i << 4) | byte2i1; + } +} + /** * Populate charCodes from bytes. Note that charCodes index 0 and 1 ("0x") are not populated. */ @@ -85,3 +102,22 @@ function bytesIntoCharCodes(bytes: Uint8Array, charCodes: number[]): void { charCodes[2 + 2 * i + 1] = second < 10 ? second + 48 : second + 87; } } + +function charCodeToByte(charCode: number): number { + // "a".charCodeAt(0) = 97, "f".charCodeAt(0) = 102 => delta = 87 + if (charCode >= 97 && charCode <= 102) { + return charCode - 87; + } + + // "A".charCodeAt(0) = 65, "F".charCodeAt(0) = 70 => delta = 55 + if (charCode >= 65 && charCode <= 70) { + return charCode - 55; + } + + // "0".charCodeAt(0) = 48, "9".charCodeAt(0) = 57 => delta = 48 + if (charCode >= 48 && charCode <= 57) { + return charCode - 48; + } + + throw new Error(`Invalid hex character code: ${charCode}`); +} diff --git a/packages/utils/src/bytes/index.ts b/packages/utils/src/bytes/index.ts index 762a33bf52..51823ae03b 100644 --- a/packages/utils/src/bytes/index.ts +++ b/packages/utils/src/bytes/index.ts @@ -1,5 +1,6 @@ import { fromHex as browserFromHex, + fromHexInto as browserFromHexInto, toHex as browserToHex, toPubkeyHex as browserToPubkeyHex, toRootHex as browserToRootHex, @@ -15,6 +16,8 @@ let toHex = browserToHex; let toRootHex = browserToRootHex; let toPubkeyHex = browserToPubkeyHex; let fromHex = browserFromHex; +// there is no fromHexInto for NodeJs as the performance of browserFromHexInto is >100x faster +const fromHexInto = browserFromHexInto; if (typeof Buffer !== "undefined") { toHex = nodeToHex; @@ -23,4 +26,4 @@ if (typeof Buffer !== "undefined") { fromHex = nodeFromHex; } -export {toHex, toRootHex, toPubkeyHex, fromHex}; +export {toHex, toRootHex, toPubkeyHex, fromHex, fromHexInto}; diff --git a/packages/utils/src/bytes/nodejs.ts b/packages/utils/src/bytes/nodejs.ts index efa7a58583..248d75f8c7 100644 --- a/packages/utils/src/bytes/nodejs.ts +++ b/packages/utils/src/bytes/nodejs.ts @@ -1,3 +1,5 @@ +import {fromHexInto as _fromHexInto} from "./browser.js"; + export function toHex(buffer: Uint8Array | Parameters[0]): string { if (Buffer.isBuffer(buffer)) { return "0x" + buffer.toString("hex"); @@ -59,3 +61,5 @@ export function fromHex(hex: string): Uint8Array { const b = Buffer.from(hex, "hex"); return new Uint8Array(b.buffer, b.byteOffset, b.length); } + +/// the performance of fromHexInto using a preallocated buffer is very bad compared to browser so I moved it to the benchmark diff --git a/packages/utils/test/perf/bytes.test.ts b/packages/utils/test/perf/bytes.test.ts index 2f37667c25..38d91a165c 100644 --- a/packages/utils/test/perf/bytes.test.ts +++ b/packages/utils/test/perf/bytes.test.ts @@ -1,11 +1,23 @@ import {bench, describe} from "@chainsafe/benchmark"; import {toHexString} from "../../src/bytes.js"; -import {toHex as browserToHex, toRootHex as browserToRootHex} from "../../src/bytes/browser.js"; -import {toHex, toRootHex} from "../../src/bytes/nodejs.js"; +import { + fromHex as browserFromHex, + fromHexInto as browserFromHexInto, + toHex as browserToHex, + toRootHex as browserToRootHex, +} from "../../src/bytes/browser.js"; +import {fromHex, toHex, toRootHex} from "../../src/bytes/nodejs.js"; describe("bytes utils", () => { const runsFactor = 1000; const blockRoot = new Uint8Array(Array.from({length: 32}, (_, i) => i)); + // FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT = 4096 * 32 = 131072 + const BLOB_LEN = 131072; + const blob = new Uint8Array(BLOB_LEN); + for (let i = 0; i < blob.length; i++) { + blob[i] = i % 256; + } + const blobHex = toHex(blob); bench({ id: "nodejs block root to RootHex using toHex", @@ -27,6 +39,25 @@ describe("bytes utils", () => { runsFactor, }); + bench({ + id: "nodejs fromhex(blob)", + fn: () => { + for (let i = 0; i < runsFactor; i++) { + fromHex(blobHex); + } + }, + }); + + const buffer = Buffer.alloc(BLOB_LEN); + bench({ + id: "nodejs fromHexInto(blob)", + fn: () => { + for (let i = 0; i < runsFactor; i++) { + nodeJsFromHexInto(blobHex, buffer); + } + }, + }); + bench({ id: "browser block root to RootHex using the deprecated toHexString", fn: () => { @@ -56,4 +87,41 @@ describe("bytes utils", () => { }, runsFactor, }); + + const buf = new Uint8Array(BLOB_LEN); + bench({ + id: "browser fromHexInto(blob)", + fn: () => { + for (let i = 0; i < runsFactor; i++) { + browserFromHexInto(blobHex, buf); + } + }, + runsFactor, + }); + + bench({ + id: "browser fromHex(blob)", + fn: () => { + for (let i = 0; i < runsFactor; i++) { + browserFromHex(blobHex); + } + }, + }); }); + +/** + * this function is so slow compared to browser's implementation we only maintain it here to compare performance + * - nodejs fromHexInto(blob) 3.562495 ops/s 280.7022 ms/op - 10 runs 3.50 s + * - browser fromHexInto(blob) 535.0952 ops/s 1.868826 ms/op - 10 runs 20.8 s + */ +function nodeJsFromHexInto(hex: string, buffer: Buffer): void { + if (hex.startsWith("0x")) { + hex = hex.slice(2); + } + + if (hex.length !== buffer.length * 2) { + throw new Error(`hex string length ${hex.length} must be exactly double the buffer length ${buffer.length}`); + } + + buffer.write(hex, "hex"); +} diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index 3877ad81e1..42c5d75115 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -3,6 +3,7 @@ import { bytesToInt, formatBytes, fromHex, + fromHexInto, intToBytes, toHex, toHexString, @@ -108,16 +109,26 @@ describe("toPubkeyHex", () => { } }); -describe("fromHex", () => { +describe("fromHex and fromHexInto", () => { const testCases: {input: string; output: Buffer | Uint8Array}[] = [ { input: "0x48656c6c6f2c20576f726c6421", output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]), }, + { + // same but with upper case + input: "0x48656C6C6F2C20576F726C6421", + output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]), + }, { input: "48656c6c6f2c20576f726c6421", output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]), }, + { + // same but with upper case + input: "48656C6C6F2C20576F726C6421", + output: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]), + }, {input: "0x", output: new Uint8Array([])}, ]; @@ -126,6 +137,14 @@ describe("fromHex", () => { expect(fromHex(input)).toEqual(output); }); } + + for (const {input, output} of testCases) { + it(`should convert hex string ${input} into provided buffer`, () => { + const buffer = new Uint8Array(output.length); + fromHexInto(input, buffer); + expect(toHex(buffer)).toBe(toHex(output)); + }); + } }); describe("toHexString", () => {