UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

146 lines (127 loc) 6.01 kB
import type {Message} from "@libp2p/gossipsub"; import type {RPC} from "@libp2p/gossipsub/message"; import type {DataTransform} from "@libp2p/gossipsub/types"; // snappyjs is better for compression for smaller payloads import xxhashFactory from "xxhash-wasm"; import {digest} from "@chainsafe/as-sha256"; import snappyWasm from "@chainsafe/snappy-wasm"; import {ForkName} from "@lodestar/params"; import {intToBytes} from "@lodestar/utils"; import {MESSAGE_DOMAIN_VALID_SNAPPY} from "./constants.js"; import {Eth2GossipsubMetrics} from "./metrics.js"; import {GossipTopicCache, getGossipSSZType} from "./topic.js"; // Load WASM const xxhash = await xxhashFactory(); // Use salt to prevent msgId from being mined for collisions const h64Seed = BigInt(Math.floor(Math.random() * 1e9)); // create singleton snappy encoder + decoder const encoder = new snappyWasm.Encoder(); const decoder = new snappyWasm.Decoder(); // Shared buffer to convert msgId to string const sharedMsgIdBuf = Buffer.alloc(20); // Cache topic -> seed to avoid per-message allocations on the hot path. // Topics are a fixed set per fork (changes only at fork boundaries). const topicSeedCache = new Map<string, bigint>(); /** * The function used to generate a gossipsub message id * We use the first 8 bytes of SHA256(data) for content addressing */ export function fastMsgIdFn(rpcMsg: RPC.Message): string { if (rpcMsg.data) { if (rpcMsg.topic) { // Use topic-derived seed to prevent cross-topic deduplication of identical messages. // SyncCommitteeMessages are published to multiple sync_committee_{subnet} topics with // identical data, so hashing only the data incorrectly deduplicates across subnets. // See https://github.com/ChainSafe/lodestar/issues/8294 let topicSeed = topicSeedCache.get(rpcMsg.topic); if (topicSeed === undefined) { topicSeed = xxhash.h64Raw(Buffer.from(rpcMsg.topic), h64Seed); topicSeedCache.set(rpcMsg.topic, topicSeed); } return xxhash.h64Raw(rpcMsg.data, topicSeed).toString(16); } return xxhash.h64Raw(rpcMsg.data, h64Seed).toString(16); } return "0000000000000000"; } export function msgIdToStrFn(msgId: Uint8Array): string { // this is the same logic to `toHex(msgId)` with better performance sharedMsgIdBuf.set(msgId); return `0x${sharedMsgIdBuf.toString("hex")}`; } /** * Only valid msgId. Messages that fail to snappy_decompress() are not tracked */ export function msgIdFn(gossipTopicCache: GossipTopicCache, msg: Message): Uint8Array { const topic = gossipTopicCache.getTopic(msg.topic); let vec: Uint8Array[]; if (topic.boundary.fork === ForkName.phase0) { // message id for phase0. // ``` // SHA256(MESSAGE_DOMAIN_VALID_SNAPPY + snappy_decompress(message.data))[:20] // ``` vec = [MESSAGE_DOMAIN_VALID_SNAPPY, msg.data]; } else { // message id for altair and subsequent future forks. // ``` // SHA256( // MESSAGE_DOMAIN_VALID_SNAPPY + // uint_to_bytes(uint64(len(message.topic))) + // message.topic + // snappy_decompress(message.data) // )[:20] // ``` // https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.7/specs/altair/p2p-interface.md#topics-and-messages vec = [MESSAGE_DOMAIN_VALID_SNAPPY, intToBytes(msg.topic.length, 8), Buffer.from(msg.topic), msg.data]; } return digest(Buffer.concat(vec)).subarray(0, 20); } export class DataTransformSnappy implements DataTransform { constructor( private readonly gossipTopicCache: GossipTopicCache, private readonly maxSizePerMessage: number, private readonly metrics: Eth2GossipsubMetrics | null ) {} /** * Takes the data published by peers on a topic and transforms the data. * Should be the reverse of outboundTransform(). Example: * - `inboundTransform()`: decompress snappy payload * - `outboundTransform()`: compress snappy payload */ inboundTransform(topicStr: string, data: Uint8Array): Uint8Array { // check uncompressed data length before we actually decompress const uncompressedDataLength = snappyWasm.decompress_len(data); if (uncompressedDataLength > this.maxSizePerMessage) { throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${this.maxSizePerMessage}`); } const topic = this.gossipTopicCache.getTopic(topicStr); const sszType = getGossipSSZType(topic); this.metrics?.dataTransform.inbound.inc({type: topic.type}); if (uncompressedDataLength < sszType.minSize) { throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} < ${sszType.minSize}`); } if (uncompressedDataLength > sszType.maxSize) { throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${sszType.maxSize}`); } // Only after sanity length checks, we can decompress the data // Using Buffer.alloc() instead of Buffer.allocUnsafe() to mitigate high GC pressure observed in some environments const uncompressedData = Buffer.alloc(uncompressedDataLength); decoder.decompress_into(data, uncompressedData); return uncompressedData; } /** * Takes the data to be published (a topic and associated data) transforms the data. The * transformed data will then be used to create a `RawGossipsubMessage` to be sent to peers. */ outboundTransform(topicStr: string, data: Uint8Array): Uint8Array { const topic = this.gossipTopicCache.getTopic(topicStr); this.metrics?.dataTransform.outbound.inc({type: topic.type}); if (data.length > this.maxSizePerMessage) { throw Error(`ssz_snappy encoded data length ${data.length} > ${this.maxSizePerMessage}`); } // Using Buffer.alloc() instead of Buffer.allocUnsafe() to mitigate high GC pressure observed in some environments const compressedData = Buffer.alloc(snappyWasm.max_compress_len(data.length)); const compressedLen = encoder.compress_into(data, compressedData); return compressedData.subarray(0, compressedLen); } }