Skip to content

Commit 7350ba3

Browse files
alexghrclaude
andauthored
feat: use native crypto to compute p2p message ID (#20846)
Replace hash.js for native sha256 when computing P2P message IDs. Fix A-583 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 43ab03a commit 7350ba3

2 files changed

Lines changed: 134 additions & 6 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { asyncPool } from '@aztec/foundation/async-pool';
2+
import { randomBytes } from '@aztec/foundation/crypto/random';
3+
import { sha256 } from '@aztec/foundation/crypto/sha256';
4+
import { MAX_L2_BLOCK_SIZE_KB, MAX_MESSAGE_SIZE_KB, MAX_TX_SIZE_KB } from '@aztec/stdlib/p2p';
5+
6+
import { createHash } from 'node:crypto';
7+
import fs from 'node:fs/promises';
8+
import path from 'node:path';
9+
import { type RecordableHistogram, createHistogram } from 'node:perf_hooks';
10+
11+
const HASH_COUNT = 20;
12+
const TOPIC = '/aztec/tx/0.1.0';
13+
14+
const MESSAGE_SIZES_KB = [1, 64, MAX_TX_SIZE_KB, MAX_L2_BLOCK_SIZE_KB, MAX_MESSAGE_SIZE_KB] as const;
15+
16+
type SizeKb = (typeof MESSAGE_SIZES_KB)[number];
17+
18+
const CONCURRENCY_LEVELS = [1, 4] as const;
19+
type CaseKey = `${SizeKb}-${(typeof CONCURRENCY_LEVELS)[number]}`;
20+
21+
const NS_PER_MS = 1e6;
22+
23+
const CASES = MESSAGE_SIZES_KB.flatMap(s => CONCURRENCY_LEVELS.map(c => [s, c] as const));
24+
25+
describe('P2P Message ID: Benchmarks', () => {
26+
let hashJsHistograms: Record<CaseKey, { h: RecordableHistogram; total: number }>;
27+
let nodeCryptoHistograms: Record<CaseKey, { h: RecordableHistogram; total: number }>;
28+
let subtleHistograms: Record<CaseKey, { h: RecordableHistogram; total: number }>;
29+
30+
let messageData: Record<SizeKb, Uint8Array>;
31+
32+
beforeAll(() => {
33+
const allKeys = CASES.map(([s, c]) => `${s}-${c}` as CaseKey);
34+
hashJsHistograms = Object.fromEntries(allKeys.map(k => [k, { h: createHistogram(), total: 0 }])) as any;
35+
nodeCryptoHistograms = Object.fromEntries(allKeys.map(k => [k, { h: createHistogram(), total: 0 }])) as any;
36+
subtleHistograms = Object.fromEntries(allKeys.map(k => [k, { h: createHistogram(), total: 0 }])) as any;
37+
38+
messageData = Object.fromEntries(MESSAGE_SIZES_KB.map(sizeKb => [sizeKb, randomBytes(sizeKb * 1024)])) as any;
39+
});
40+
41+
afterAll(async () => {
42+
const implementations = [
43+
{ key: 'hash.js', label: 'hashJs.sha256 x' + HASH_COUNT, histograms: hashJsHistograms },
44+
{ key: 'node-crypto', label: 'crypto.createHash x' + HASH_COUNT, histograms: nodeCryptoHistograms },
45+
{ key: 'web-crypto', label: 'globalThis.crypto.subtle.digest x' + HASH_COUNT, histograms: subtleHistograms },
46+
];
47+
48+
const data: { name: string; value: number; unit: string }[] = [];
49+
for (const [sizeKb, concurrency] of CASES) {
50+
const key: CaseKey = `${sizeKb}-${concurrency}`;
51+
for (const impl of implementations) {
52+
const { h, total } = impl.histograms[key];
53+
data.push({ name: `MsgId/${impl.key}/x${concurrency}/${sizeKb}kb/avg`, value: h.mean, unit: 'ms' });
54+
data.push({ name: `MsgId/${impl.key}/x${concurrency}/${sizeKb}kb/p50`, value: h.percentile(50), unit: 'ms' });
55+
data.push({ name: `MsgId/${impl.key}/x${concurrency}/${sizeKb}kb/p99`, value: h.percentile(99), unit: 'ms' });
56+
data.push({ name: `MsgId/${impl.key}/x${concurrency}/${sizeKb}kb/sum`, value: total, unit: 'ms' });
57+
}
58+
}
59+
60+
if (process.env.BENCH_OUTPUT) {
61+
await fs.mkdir(path.dirname(process.env.BENCH_OUTPUT), { recursive: true });
62+
await fs.writeFile(process.env.BENCH_OUTPUT, JSON.stringify(data, null, 2));
63+
} else if (process.env.BENCH_OUTPUT_MD) {
64+
await fs.mkdir(path.dirname(process.env.BENCH_OUTPUT_MD), { recursive: true });
65+
await using f = await fs.open(process.env.BENCH_OUTPUT_MD, 'w');
66+
await f.write('| Function | CONCURRENCY | Size (KB) | Avg (ms) | P50 (ms) | P99 (ms) | TOTAL (ms) |\n');
67+
await f.write('|----------|---|-----------|----------|----------|----------|------------|\n');
68+
for (const [sizeKb, concurrency] of CASES) {
69+
const key: CaseKey = `${sizeKb}-${concurrency}`;
70+
for (const impl of implementations) {
71+
const { h, total } = impl.histograms[key];
72+
await f.write(
73+
`| ${impl.label} | ${concurrency} | ${sizeKb} | ${h.mean} | ${h.percentile(50)} | ${h.percentile(99)} | ${total} |\n`,
74+
);
75+
}
76+
}
77+
}
78+
});
79+
80+
it.each(CASES)('hash.js sha256: %d KB x%d', async (sizeKb, concurrency) => {
81+
const data = messageData[sizeKb as SizeKb];
82+
const key: CaseKey = `${sizeKb}-${concurrency}`;
83+
const res = hashJsHistograms[key];
84+
85+
const testStart = process.hrtime.bigint();
86+
await asyncPool(concurrency, Array(HASH_COUNT), () => {
87+
const start = process.hrtime.bigint();
88+
sha256(Buffer.concat([Buffer.from(TOPIC), data])).subarray(0, 20);
89+
const elapsed = Number(process.hrtime.bigint() - start) / NS_PER_MS;
90+
res.h.record(Math.trunc(Math.max(1, elapsed)));
91+
return Promise.resolve();
92+
});
93+
res.total = Number(process.hrtime.bigint() - testStart) / NS_PER_MS;
94+
});
95+
96+
it.each(CASES)('node:crypto createHash: %d KB x%d', async (sizeKb, concurrency) => {
97+
const data = messageData[sizeKb as SizeKb];
98+
const key: CaseKey = `${sizeKb}-${concurrency}`;
99+
const res = nodeCryptoHistograms[key];
100+
101+
const testStart = process.hrtime.bigint();
102+
await asyncPool(concurrency, Array(HASH_COUNT), () => {
103+
const start = process.hrtime.bigint();
104+
createHash('sha256').update(TOPIC).update(data).digest().subarray(0, 20);
105+
const elapsed = Number(process.hrtime.bigint() - start) / NS_PER_MS;
106+
res.h.record(Math.trunc(Math.max(1, elapsed)));
107+
return Promise.resolve();
108+
});
109+
res.total = Number(process.hrtime.bigint() - testStart) / NS_PER_MS;
110+
});
111+
112+
it.each(CASES)('crypto.subtle.digest parallel: %d KB x%d', async (sizeKb, concurrency) => {
113+
const data = messageData[sizeKb as SizeKb];
114+
const concat = Buffer.concat([Buffer.from(TOPIC), data]);
115+
const key: CaseKey = `${sizeKb}-${concurrency}`;
116+
const res = subtleHistograms[key];
117+
118+
const testStart = process.hrtime.bigint();
119+
120+
await asyncPool(concurrency, Array(HASH_COUNT), async () => {
121+
const start = process.hrtime.bigint();
122+
await crypto.subtle.digest('SHA-256', concat).then(buf => Buffer.from(buf).subarray(0, 20));
123+
const elapsed = Number(process.hrtime.bigint() - start) / NS_PER_MS;
124+
res.h.record(Math.trunc(Math.max(1, elapsed)));
125+
});
126+
127+
res.total = Number(process.hrtime.bigint() - testStart) / NS_PER_MS;
128+
});
129+
});

yarn-project/p2p/src/services/encoding.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Taken from lodestar: https://github.com/ChainSafe/lodestar
2-
import { sha256 } from '@aztec/foundation/crypto/sha256';
32
import { createLogger } from '@aztec/foundation/log';
43
import { MAX_TX_SIZE_KB, TopicType, getTopicFromString } from '@aztec/stdlib/p2p';
54

65
import type { RPC } from '@chainsafe/libp2p-gossipsub/message';
76
import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types';
87
import type { Message } from '@libp2p/interface';
8+
import { webcrypto } from 'node:crypto';
99
import { compressSync, uncompressSync } from 'snappy';
1010
import xxhashFactory from 'xxhash-wasm';
1111

@@ -44,11 +44,10 @@ export function msgIdToStrFn(msgId: Uint8Array): string {
4444
* @param message - The libp2p message
4545
* @returns The message identifier
4646
*/
47-
export function getMsgIdFn(message: Message) {
48-
const { topic } = message;
49-
50-
const vec = [Buffer.from(topic), message.data];
51-
return sha256(Buffer.concat(vec)).subarray(0, 20);
47+
export async function getMsgIdFn({ topic, data }: Message): Promise<Uint8Array> {
48+
const buffer = Buffer.concat([Buffer.from(topic), data]);
49+
const hash = await webcrypto.subtle.digest('SHA-256', buffer);
50+
return Buffer.from(hash.slice(0, 20));
5251
}
5352

5453
const DefaultMaxSizesKb: Record<TopicType, number> = {

0 commit comments

Comments
 (0)