diff --git a/packages/portalnetwork/src/networks/beacon/beacon.ts b/packages/portalnetwork/src/networks/beacon/beacon.ts index adea563a5..3994794c7 100644 --- a/packages/portalnetwork/src/networks/beacon/beacon.ts +++ b/packages/portalnetwork/src/networks/beacon/beacon.ts @@ -21,9 +21,10 @@ import debug from 'debug' import { getENR, shortId } from '../../util/util.js' import { FoundContent, - MAX_PACKET_SIZE, + MAX_UDP_PACKET_SIZE, RequestCode, encodeWithVariantPrefix, + getTalkReqOverhead, randUint16, } from '../../wire/index.js' import { ContentMessageType, MessageCodes, PortalWireMessageType } from '../../wire/types.js' @@ -573,7 +574,10 @@ export class BeaconLightClientNetwork extends BaseNetwork { const value = await this.findContentLocally(decodedContentMessage.contentKey) if (!value) { await this.enrResponse(decodedContentMessage.contentKey, src, requestId) - } else if (value !== undefined && value.length < MAX_PACKET_SIZE) { + } else if ( + value !== undefined && + value.length < MAX_UDP_PACKET_SIZE - getTalkReqOverhead(hexToBytes(this.networkId).byteLength) + ) { this.logger( 'Found value for requested content ' + bytesToHex(decodedContentMessage.contentKey) + diff --git a/packages/portalnetwork/src/networks/beacon/types.ts b/packages/portalnetwork/src/networks/beacon/types.ts index b4ec7d34f..c0227a1e2 100644 --- a/packages/portalnetwork/src/networks/beacon/types.ts +++ b/packages/portalnetwork/src/networks/beacon/types.ts @@ -54,7 +54,11 @@ export const HistoricalSummariesKey = new ContainerType({ epoch: new UintBigintT export const HistoricalSummariesStateProof = new VectorCompositeType(Bytes32Type, 5) -export const HistoricalSummariesWithProof = new ContainerType( +export const HistoricalSummariesWithProof = new ContainerType<{ + epoch: UintBigintType + historicalSummaries: typeof ssz.capella.BeaconState.fields.historicalSummaries + proof: typeof HistoricalSummariesStateProof +}>( { epoch: new UintBigintType(8), historicalSummaries: ssz.capella.BeaconState.fields.historicalSummaries, diff --git a/packages/portalnetwork/src/networks/history/history.ts b/packages/portalnetwork/src/networks/history/history.ts index e29fa6acf..24a229c0a 100644 --- a/packages/portalnetwork/src/networks/history/history.ts +++ b/packages/portalnetwork/src/networks/history/history.ts @@ -5,17 +5,19 @@ import debug from 'debug' import type { BaseNetworkConfig, ContentLookupResponse, + EphemeralHeaderKeyValues, FindContentMessage, INodeAddress, } from '../../index.js' import { BasicRadius, + BiMap, ClientInfoAndCapabilities, ContentMessageType, FoundContent, HistoricalSummariesBlockProof, HistoryRadius, - MAX_PACKET_SIZE, + MAX_UDP_PACKET_SIZE, MessageCodes, PingPongPayloadExtensions, PortalWireMessageType, @@ -23,6 +25,7 @@ import { decodeHistoryNetworkContentKey, decodeReceipts, encodeClientInfo, + getTalkReqOverhead, randUint16, reassembleBlock, saveReceipts, @@ -59,7 +62,7 @@ export class HistoryNetwork extends BaseNetwork { networkId: NetworkId.HistoryNetwork networkName = 'HistoryNetwork' logger: Debugger - public ephemeralHeaderIndex: Map // Map of slot numbers to hashes + public ephemeralHeaderIndex: BiMap // Map of block number to block hashes public blockHashIndex: Map constructor({ client, db, radius, maxStorage }: BaseNetworkConfig) { super({ client, networkId: NetworkId.HistoryNetwork, db, radius, maxStorage }) @@ -71,7 +74,7 @@ export class HistoryNetwork extends BaseNetwork { this.logger = debug(this.enr.nodeId.slice(0, 5)).extend('Portal').extend('HistoryNetwork') this.routingTable.setLogger(this.logger) this.blockHashIndex = new Map() - this.ephemeralHeaderIndex = new Map() + this.ephemeralHeaderIndex = new BiMap() } public blockNumberToHash(blockNumber: bigint): Uint8Array | undefined { @@ -237,7 +240,6 @@ export class HistoryNetwork extends BaseNetwork { let deserializedProof: ReturnType try { deserializedProof = HistoricalSummariesBlockProof.deserialize(proof) - console.log(HistoricalSummariesBlockProof.toJson(deserializedProof)) } catch (err: any) { this.logger(`invalid proof for block ${bytesToHex(header.hash())}`) throw new Error(`invalid proof for block ${bytesToHex(header.hash())}`) @@ -305,18 +307,6 @@ export class HistoryNetwork extends BaseNetwork { * @returns the value of the FOUNDCONTENT response or undefined */ public sendFindContent = async (enr: ENR, key: Uint8Array) => { - if (key[0] === HistoryNetworkContentType.EphemeralHeader) { - const beacon = this.portal.network()['0x500c'] - if ( - beacon === undefined || - beacon.lightClient?.status === RunStatusCode.uninitialized || - beacon.lightClient?.status === RunStatusCode.stopped - ) { - const errorMessage = 'Cannot verify ephemeral headers when beacon network is not running' - this.logger.extend('FINDCONTENT')(errorMessage) - throw new Error(errorMessage) - } - } this.portal.metrics?.findContentMessagesSent.inc() const findContentMsg: FindContentMessage = { contentKey: key } const payload = PortalWireMessageType.serialize({ @@ -394,12 +384,82 @@ export class HistoryNetwork extends BaseNetwork { )}`, ) - // TODO: Add specific support for retrieving ephemeral headers - const value = await this.findContentLocally(decodedContentMessage.contentKey) + const contentKey = decodeHistoryNetworkContentKey(decodedContentMessage.contentKey) + let value: Uint8Array | undefined + if (contentKey.contentType === HistoryNetworkContentType.EphemeralHeader) { + if (contentKey.keyOpt.ancestorCount < 0 || contentKey.keyOpt.ancestorCount > 255) { + const errorMessage = `received invalid ephemeral headers request with invalid ancestorCount: expected 0 <= 255, got ${contentKey.keyOpt.ancestorCount}` + this.logger.extend('FOUNDCONTENT')(errorMessage) + throw new Error(errorMessage) + } + this.logger.extend('FOUNDCONTENT')( + `Received ephemeral headers request for block ${bytesToHex(contentKey.keyOpt.blockHash)} with ancestorCount ${contentKey.keyOpt.ancestorCount}`, + ) + // Retrieve the starting header from the FINDCONTENT request + const headerKey = getEphemeralHeaderDbKey(contentKey.keyOpt.blockHash) + const firstHeader = await this.findContentLocally(headerKey) + + if (firstHeader === undefined) { + // If we don't have the requested header, send an empty payload + // We never send an ENRs response for ephemeral headers + value = undefined + const emptyHeaderPayload = EphemeralHeaderPayload.serialize([]) + const messagePayload = ContentMessageType.serialize({ + selector: FoundContent.CONTENT, + value: emptyHeaderPayload, + }) + this.logger.extend('FOUNDCONTENT')( + `Header not found for ${bytesToHex(contentKey.keyOpt.blockHash)}, sending empty ephemeral headers response to ${shortId(src.nodeId)}`, + ) + await this.sendResponse( + src, + requestId, + concatBytes(Uint8Array.from([MessageCodes.CONTENT]), messagePayload), + ) + return + } else { + this.logger.extend('FOUNDCONTENT')( + `Header found for ${bytesToHex(contentKey.keyOpt.blockHash)}, assembling ephemeral headers response to ${shortId(src.nodeId)}`, + ) + // We have the requested header so begin assembling the payload + const headersList = [firstHeader] + const firstHeaderNumber = this.ephemeralHeaderIndex.getByValue( + bytesToHex(contentKey.keyOpt.blockHash), + ) + for (let x = 1; x <= contentKey.keyOpt.ancestorCount; x++) { + // Determine if we have the ancestor header at block number `firstHeaderNumber - x` + const ancestorNumber = firstHeaderNumber! - BigInt(x) + const ancestorHash = this.ephemeralHeaderIndex.getByKey(ancestorNumber) + if (ancestorHash === undefined) + break // Stop looking for more ancestors if we don't have the current one in the index + else { + const ancestorKey = getEphemeralHeaderDbKey(hexToBytes(ancestorHash)) + const ancestorHeader = await this.findContentLocally(ancestorKey) + if (ancestorHeader === undefined) { + // This would only happen if our index gets out of sync with the DB + // Stop looking for more ancestors if we don't have the current one in the DB + this.ephemeralHeaderIndex.delete(ancestorNumber) + break + } else { + headersList.push(ancestorHeader) + } + } + } + this.logger.extend('FOUNDCONTENT')( + `found ${headersList.length - 1} ancestor headers for ${bytesToHex(contentKey.keyOpt.blockHash)}`, + ) + value = EphemeralHeaderPayload.serialize(headersList) + } + } else { + value = await this.findContentLocally(decodedContentMessage.contentKey) + } if (!value) { await this.enrResponse(decodedContentMessage.contentKey, src, requestId) - } else if (value instanceof Uint8Array && value.length < MAX_PACKET_SIZE) { - this.logger( + } else if ( + value instanceof Uint8Array && + value.length < MAX_UDP_PACKET_SIZE - getTalkReqOverhead(hexToBytes(this.networkId).byteLength) + ) { + this.logger.extend('FOUNDCONTENT')( 'Found value for requested content ' + bytesToHex(decodedContentMessage.contentKey) + ' ' + @@ -407,7 +467,7 @@ export class HistoryNetwork extends BaseNetwork { `...`, ) const payload = ContentMessageType.serialize({ - selector: 1, + selector: FoundContent.CONTENT, value, }) this.logger.extend('CONTENT')(`Sending requested content to ${src.nodeId}`) @@ -496,14 +556,18 @@ export class HistoryNetwork extends BaseNetwork { case HistoryNetworkContentType.EphemeralHeader: { const payload = EphemeralHeaderPayload.deserialize(value) + if (payload.length === 0) { + this.logger.extend('STORE')('Received empty ephemeral header payload') + return + } try { // Verify first header matches requested header const firstHeader = BlockHeader.fromRLPSerializedHeader(payload[0], { setHardfork: true }) const requestedHeaderHash = decodeHistoryNetworkContentKey(contentKey) - .keyOpt as Uint8Array - if (!equalsBytes(firstHeader.hash(), requestedHeaderHash)) { + .keyOpt as EphemeralHeaderKeyValues + if (!equalsBytes(firstHeader.hash(), requestedHeaderHash.blockHash)) { // TODO: Should we ban/mark down the score of peers who send junk payload? - const errorMessage = `invalid ephemeral header payload; requested ${bytesToHex(requestedHeaderHash)}, got ${bytesToHex(firstHeader.hash())}` + const errorMessage = `invalid ephemeral header payload; requested ${bytesToHex(requestedHeaderHash.blockHash)}, got ${bytesToHex(firstHeader.hash())}` this.logger(errorMessage) throw new Error(errorMessage) } diff --git a/packages/portalnetwork/src/networks/history/types.ts b/packages/portalnetwork/src/networks/history/types.ts index e5f7ae080..e9851adce 100644 --- a/packages/portalnetwork/src/networks/history/types.ts +++ b/packages/portalnetwork/src/networks/history/types.ts @@ -225,3 +225,8 @@ export const EphemeralHeaderPayload = new ListCompositeType( BlockHeader, MAX_EPHEMERAL_HEADERS_PAYLOAD, ) + +export type EphemeralHeaderKeyValues = { + blockHash: Uint8Array + ancestorCount: number +} diff --git a/packages/portalnetwork/src/networks/history/util.ts b/packages/portalnetwork/src/networks/history/util.ts index 131d97957..0396888a4 100644 --- a/packages/portalnetwork/src/networks/history/util.ts +++ b/packages/portalnetwork/src/networks/history/util.ts @@ -41,6 +41,7 @@ import type { WithdrawalBytes } from '@ethereumjs/util' import type { ForkConfig } from '@lodestar/config' import type { HistoryNetwork } from './history.js' import type { BlockBodyContent, Witnesses } from './types.js' +import type { EphemeralHeaderKeyValues } from '../history/types.js' export const BlockHeaderByNumberKey = (blockNumber: bigint) => { return Uint8Array.from([ @@ -57,7 +58,7 @@ export const BlockHeaderByNumberKey = (blockNumber: bigint) => { */ export const getContentKey = ( contentType: HistoryNetworkContentType, - key: Uint8Array | bigint | { blockHash: Uint8Array; ancestorCount: number }, + key: Uint8Array | bigint | EphemeralHeaderKeyValues, ): Uint8Array => { let encodedKey switch (contentType) { @@ -120,12 +121,15 @@ export const decodeHistoryNetworkContentKey = ( | HistoryNetworkContentType.BlockHeader | HistoryNetworkContentType.BlockBody | HistoryNetworkContentType.Receipt - | HistoryNetworkContentType.EphemeralHeader keyOpt: Uint8Array } | { contentType: HistoryNetworkContentType.BlockHeaderByNumber keyOpt: bigint + } + | { + contentType: HistoryNetworkContentType.EphemeralHeader + keyOpt: EphemeralHeaderKeyValues } => { const contentType: HistoryNetworkContentType = contentKey[0] switch (contentType) { @@ -140,7 +144,7 @@ export const decodeHistoryNetworkContentKey = ( const key = EphemeralHeaderKey.deserialize(contentKey.slice(1)) return { contentType, - keyOpt: key.blockHash, + keyOpt: key, } } default: { diff --git a/packages/portalnetwork/src/networks/network.ts b/packages/portalnetwork/src/networks/network.ts index 61e555d71..610ee5382 100644 --- a/packages/portalnetwork/src/networks/network.ts +++ b/packages/portalnetwork/src/networks/network.ts @@ -35,7 +35,7 @@ import { ContentMessageType, ErrorPayload, HistoryRadius, - MAX_PACKET_SIZE, + MAX_UDP_PACKET_SIZE, MessageCodes, NodeLookup, PingPongErrorCodes, @@ -48,6 +48,7 @@ import { encodeClientInfo, encodeWithVariantPrefix, generateRandomNodeIdAtDistance, + getTalkReqOverhead, randUint16, shortId, } from '../index.js' @@ -211,7 +212,6 @@ export abstract class BaseNetwork extends EventEmitter { public async handle(message: ITalkReqMessage, src: INodeAddress) { const id = message.id - const network = message.protocol const request = message.request const deserialized = PortalWireMessageType.deserialize(request) const decoded = deserialized.value @@ -378,7 +378,9 @@ export abstract class BaseNetwork extends EventEmitter { if (this.capabilities.includes(pingMessage.payloadType)) { switch (pingMessage.payloadType) { case PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES: { - const { DataRadius, Capabilities, ClientInfo } = ClientInfoAndCapabilities.deserialize(pingMessage.customPayload) + const { DataRadius, Capabilities, ClientInfo } = ClientInfoAndCapabilities.deserialize( + pingMessage.customPayload, + ) this.routingTable.updateRadius(src.nodeId, DataRadius) this.portal.enrCache.updateNodeFromPing(src, this.networkId, { capabilities: Capabilities, @@ -761,7 +763,10 @@ export abstract class BaseNetwork extends EventEmitter { const value = await this.findContentLocally(decodedContentMessage.contentKey) if (!value) { await this.enrResponse(decodedContentMessage.contentKey, src, requestId) - } else if (value instanceof Uint8Array && value.length < MAX_PACKET_SIZE) { + } else if ( + value instanceof Uint8Array && + value.length < MAX_UDP_PACKET_SIZE - getTalkReqOverhead(hexToBytes(this.networkId).byteLength) + ) { this.logger( 'Found value for requested content ' + bytesToHex(decodedContentMessage.contentKey) + @@ -817,7 +822,11 @@ export abstract class BaseNetwork extends EventEmitter { if (encodedEnrs.length > 0) { this.logger.extend('FINDCONTENT')(`Found ${encodedEnrs.length} closer to content`) // TODO: Add capability to send multiple TALKRESP messages if # ENRs exceeds packet size - while (encodedEnrs.length > 0 && arrayByteLength(encodedEnrs) > MAX_PACKET_SIZE) { + while ( + encodedEnrs.length > 0 && + arrayByteLength(encodedEnrs) > + MAX_UDP_PACKET_SIZE - getTalkReqOverhead(hexToBytes(this.networkId).byteLength) + ) { // Remove ENRs until total ENRs less than 1200 bytes encodedEnrs.pop() } diff --git a/packages/portalnetwork/src/util/util.ts b/packages/portalnetwork/src/util/util.ts index f3d41d99f..c70b10915 100644 --- a/packages/portalnetwork/src/util/util.ts +++ b/packages/portalnetwork/src/util/util.ts @@ -78,3 +78,77 @@ export const getENR = (routingTable: RoutingTable, enrOrId: string) => { : routingTable.getWithPending(enrOrId.slice(2))?.value return enr } + +/** + * Bidirectional map that maintains a one-to-one correspondence between keys and values. + * When a key-value pair is added, any existing pairs with the same key or value are removed. + * @template T The type of the keys + * @template U The type of the values + * @note Built by robots with oversight by acolytec3 + * @note Uint8Arrays can not be used as keys or values in this class since bytes are not directly comparable + */ +export class BiMap { + #forward = new Map() + #reverse = new Map() + + /** + * Sets a key-value pair in the map. If either the key or value already exists, + * the old mapping is removed before the new one is added. + * @param key The key to set + * @param value The value to set + */ + set(key: T, value: U): void { + const oldValue = this.#forward.get(key) + if (oldValue !== undefined) { + this.#reverse.delete(oldValue) + } + + const oldKey = this.#reverse.get(value) + if (oldKey !== undefined) { + this.#forward.delete(oldKey) + } + + this.#forward.set(key, value) + this.#reverse.set(value, key) + } + + /** + * Gets a value by its key + * @param key The key to look up + * @returns The associated value, or undefined if the key doesn't exist + */ + getByKey(key: T): U | undefined { + return this.#forward.get(key) + } + + /** + * Gets a key by its value + * @param value The value to look up + * @returns The associated key, or undefined if the value doesn't exist + */ + getByValue(value: U): T | undefined { + return this.#reverse.get(value) + } + + /** + * Removes a key-value pair from the map + * @param key The key to remove + * @returns true if a pair was removed, false if the key didn't exist + */ + delete(key: T): boolean { + const value = this.#forward.get(key) + if (value === undefined) return false + + this.#forward.delete(key) + this.#reverse.delete(value) + return true + } + + /** + * Gets the size of the map + * @returns the size of the map + */ + get size(): number { + return this.#forward.size + } +} diff --git a/packages/portalnetwork/src/wire/utp/Utils/constants.ts b/packages/portalnetwork/src/wire/utp/Utils/constants.ts index ff573af6b..5092171e1 100644 --- a/packages/portalnetwork/src/wire/utp/Utils/constants.ts +++ b/packages/portalnetwork/src/wire/utp/Utils/constants.ts @@ -24,19 +24,7 @@ export const AUTO_ACK_SMALLER_THAN_ACK_NUMBER: boolean = true export const MINIMUM_DIFFERENCE_TIMESTAMP_MICROSEC: number = 120000000 export const DEFAULT_PACKET_SIZE = 512 -export const MAX_PACKET_SIZE: number = 1225 -export const MIN_PACKET_SIZE: number = 150 -export const MINIMUM_MTU: number = 576 -export const SEND_IN_BURST: boolean = true -export const MAX_BURST_SEND: number = 5 -export const MIN_SKIP_PACKET_BEFORE_RESEND: number = 3 -export const MICROSECOND_WAIT_BETWEEN_BURSTS: number = 28000 -export const TIME_WAIT_AFTER_LAST_PACKET: number = 3000000 -export const ONLY_POSITIVE_GAIN: boolean = false -export const DEBUG: boolean = false -export const MAX_UTP_PACKET_LENGTH = MAX_PACKET_SIZE -export const MAX_UDP_HEADER_LENGTH = 48 -export const DEF_HEADER_LENGTH = 20 +export const MAX_UDP_PACKET_SIZE: number = 1280 export const startingNrs: Record = { [RequestCode.FOUNDCONTENT_WRITE]: { seqNr: randUint16(), ackNr: 0 }, diff --git a/packages/portalnetwork/src/wire/utp/Utils/index.ts b/packages/portalnetwork/src/wire/utp/Utils/index.ts index bbdd933dd..6fb286316 100644 --- a/packages/portalnetwork/src/wire/utp/Utils/index.ts +++ b/packages/portalnetwork/src/wire/utp/Utils/index.ts @@ -1,3 +1,4 @@ export * from './constants.js' export * from './math.js' export * from './variantPrefix.js' +export * from './packet.js' diff --git a/packages/portalnetwork/src/wire/utp/Utils/packet.ts b/packages/portalnetwork/src/wire/utp/Utils/packet.ts new file mode 100644 index 000000000..3c518bc79 --- /dev/null +++ b/packages/portalnetwork/src/wire/utp/Utils/packet.ts @@ -0,0 +1,21 @@ +/** + * Compute the number of bytes required for a TALKREQ message header + * @param protocolIdLen is the length of the protocol ID + * @returns the number of bytes required for a TALKREQ message header + * + * @note Shamelessly copied from [Fluffy](https://github.com/status-im/nimbus-eth1/blob/45767278174a48521de46f029f6e66dc526880f6/fluffy/network/wire/messages.nim#L179) + */ + +export const getTalkReqOverhead = (protocolIdLen: number): number => { + return ( + 16 + // IV size + 55 + // header size + 1 + // talkReq msg id + 3 + // rlp encoding outer list, max length will be encoded in 2 bytes + 9 + // request id (max = 8) + 1 byte from rlp encoding byte string + protocolIdLen + // bytes length of protocolid (e.g. 0x500b for History Network) + 1 + // + 1 is necessary due to rlp encoding of byte string + 3 + // rlp encoding response byte string, max length in 2 bytes + 16 // HMAC + ) +} diff --git a/packages/portalnetwork/test/integration/ephemeralHeaders.spec.ts b/packages/portalnetwork/test/integration/ephemeralHeaders.spec.ts new file mode 100644 index 000000000..9e89cda2d --- /dev/null +++ b/packages/portalnetwork/test/integration/ephemeralHeaders.spec.ts @@ -0,0 +1,121 @@ +import { SignableENR } from '@chainsafe/enr' +import type { BlockHeader, JsonRpcBlock } from '@ethereumjs/block' +import { Block } from '@ethereumjs/block' +import { hexToBytes, randomBytes } from '@ethereumjs/util' +import { keys } from '@libp2p/crypto' +import { multiaddr } from '@multiformats/multiaddr' +import { assert, beforeAll, describe, it } from 'vitest' +import { + EphemeralHeaderPayload, + HistoryNetworkContentType, + NetworkId, + PortalNetwork, + getContentKey, +} from '../../src/index.js' +import latestBlocks from '../networks/history/testData/latest3Blocks.json' + +describe('should be able to retrieve ephemeral headers from a peer', () => { + let headers: BlockHeader[] + let headerPayload: Uint8Array + let contentKey: Uint8Array + beforeAll(() => { + headers = [] + headers.push(Block.fromRPC(latestBlocks[0] as JsonRpcBlock, [], { setHardfork: true }).header) + headers.push(Block.fromRPC(latestBlocks[1] as JsonRpcBlock, [], { setHardfork: true }).header) + headers.push(Block.fromRPC(latestBlocks[2] as JsonRpcBlock, [], { setHardfork: true }).header) + headerPayload = EphemeralHeaderPayload.serialize(headers.map((h) => h.serialize())) + contentKey = getContentKey(HistoryNetworkContentType.EphemeralHeader, { + blockHash: headers[0].hash(), + ancestorCount: headers.length - 1, + }) + }) + it('should be able to retrieve ephemeral headers from a peer', async () => { + const privateKeys = [ + '0x0a2700250802122102273097673a2948af93317235d2f02ad9cf3b79a34eeb37720c5f19e09f11783c12250802122102273097673a2948af93317235d2f02ad9cf3b79a34eeb37720c5f19e09f11783c1a2408021220aae0fff4ac28fdcdf14ee8ecb591c7f1bc78651206d86afe16479a63d9cb73bd', + '0x0a27002508021221039909a8a7e81dbdc867480f0eeb7468189d1e7a1dd7ee8a13ee486c8cbd743764122508021221039909a8a7e81dbdc867480f0eeb7468189d1e7a1dd7ee8a13ee486c8cbd7437641a2408021220c6eb3ae347433e8cfe7a0a195cc17fc8afcd478b9fb74be56d13bccc67813130', + ] + + const pk1 = keys.privateKeyFromProtobuf(hexToBytes(privateKeys[0]).slice(-36)) + const enr1 = SignableENR.createFromPrivateKey(pk1) + const pk2 = keys.privateKeyFromProtobuf(hexToBytes(privateKeys[1]).slice(-36)) + const enr2 = SignableENR.createFromPrivateKey(pk2) + const initMa: any = multiaddr(`/ip4/127.0.0.1/udp/3198`) + enr1.setLocationMultiaddr(initMa) + const initMa2: any = multiaddr(`/ip4/127.0.0.1/udp/3199`) + enr2.setLocationMultiaddr(initMa2) + const node1 = await PortalNetwork.create({ + supportedNetworks: [ + { networkId: NetworkId.HistoryNetwork }, + { networkId: NetworkId.BeaconChainNetwork }, + ], + config: { + enr: enr1, + bindAddrs: { + ip4: initMa, + }, + privateKey: pk1, + }, + }) + + const node2 = await PortalNetwork.create({ + supportedNetworks: [ + { networkId: NetworkId.HistoryNetwork }, + { networkId: NetworkId.BeaconChainNetwork }, + ], + config: { + enr: enr2, + bindAddrs: { + ip4: initMa2, + }, + privateKey: pk2, + }, + }) + await node1.start() + await node2.start() + const network1 = node1.network()['0x500b'] + await network1!.store(contentKey, headerPayload) + const network2 = node2.network()['0x500b'] + const res = await network2!.sendFindContent(node1.discv5.enr.toENR(), contentKey) + assert.exists(res) + if ('content' in res!) { + const payload = EphemeralHeaderPayload.deserialize(res.content) + assert.equal(payload.length, headers.length) + assert.deepEqual(payload[0], headers[0].serialize()) + assert.deepEqual(payload[1], headers[1].serialize()) + assert.deepEqual(payload[2], headers[2].serialize()) + } else { + assert.fail('Expected content in response') + } + + // Verify that we get a single ancestor for a content key with an ancestor count of 1 + const contentKeyForOneAncestor = getContentKey(HistoryNetworkContentType.EphemeralHeader, { + blockHash: headers[0].hash(), + ancestorCount: 1, + }) + + const res2 = await network2!.sendFindContent(node1.discv5.enr.toENR(), contentKeyForOneAncestor) + assert.exists(res2) + if ('content' in res2!) { + const payload = EphemeralHeaderPayload.deserialize(res2.content) + assert.equal(payload.length, 2, 'should only get a single ancestor') + } else { + assert.fail('Expected content in response') + } + + // Verify that we get an empty ephemeral headers payload for a random blockhash + const res3 = await network2!.sendFindContent( + node1.discv5.enr.toENR(), + getContentKey(HistoryNetworkContentType.EphemeralHeader, { + blockHash: randomBytes(32), + ancestorCount: 255, + }), + ) + assert.exists(res3) + if ('content' in res3!) { + const payload = EphemeralHeaderPayload.deserialize(res3.content) + assert.equal(payload.length, 0, 'should not get any headers for a random blockhash') + } else { + assert.fail('Expected content in response') + } + }) +}) diff --git a/packages/portalnetwork/test/networks/history/ephemeralHeader.spec.ts b/packages/portalnetwork/test/networks/history/ephemeralHeader.spec.ts index db4a3fc84..4a014dd83 100644 --- a/packages/portalnetwork/test/networks/history/ephemeralHeader.spec.ts +++ b/packages/portalnetwork/test/networks/history/ephemeralHeader.spec.ts @@ -1,7 +1,7 @@ -import { assert, beforeAll, describe, it } from 'vitest' -import { Block } from '@ethereumjs/block' import type { BlockHeader, JsonRpcBlock } from '@ethereumjs/block' -import latestBlocks from './testData/latest3Blocks.json' +import { Block } from '@ethereumjs/block' +import { bytesToHex, hexToBytes } from '@ethereumjs/util' +import { assert, beforeAll, describe, it } from 'vitest' import { EphemeralHeaderPayload, HistoryNetworkContentType, @@ -11,7 +11,8 @@ import { getContentKey, getEphemeralHeaderDbKey, } from '../../../src/index.js' -import { bytesToHex, hexToBytes } from '@ethereumjs/util' +import latestBlocks from './testData/latest3Blocks.json' + describe('ephemeral header handling', () => { let headers: BlockHeader[] let headerPayload: Uint8Array @@ -24,18 +25,18 @@ describe('ephemeral header handling', () => { headerPayload = EphemeralHeaderPayload.serialize(headers.map((h) => h.serialize())) contentKey = getContentKey(HistoryNetworkContentType.EphemeralHeader, { blockHash: headers[0].hash(), - ancestorCount: headers.length, + ancestorCount: headers.length - 1, }) }) it('should be able to store a valid ephemeral header payload', async () => { const node = await PortalNetwork.create({}) const network = node.network()['0x500b'] - await network?.store(contentKey, headerPayload) + await network!.store(contentKey, headerPayload) const storedHeaderPayload = await network?.get(getEphemeralHeaderDbKey(headers[0].hash())) assert.deepEqual(hexToBytes(storedHeaderPayload!), headers[0].serialize()) - assert.equal( - network?.ephemeralHeaderIndex.get(headers[1].number), + assert.deepEqual( + network!.ephemeralHeaderIndex.getByKey(headers[1].number), bytesToHex(headers[1].hash()), ) }) diff --git a/packages/portalnetwork/test/util/bimap.spec.ts b/packages/portalnetwork/test/util/bimap.spec.ts new file mode 100644 index 000000000..c16631440 --- /dev/null +++ b/packages/portalnetwork/test/util/bimap.spec.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' +import { BiMap } from '../../src/index.js' + +describe('BiMap', () => { + it('should be able to set, get, and delete values', () => { + const bimap = new BiMap() + bimap.set(1, 'a') + bimap.set(2, 'b') + bimap.set(3, 'c') + expect(bimap.getByKey(1)).toBe('a') + expect(bimap.getByKey(2)).toBe('b') + expect(bimap.getByKey(3)).toBe('c') + expect(bimap.getByValue('a')).toBe(1) + expect(bimap.getByValue('b')).toBe(2) + expect(bimap.getByValue('c')).toBe(3) + bimap.delete(2) + expect(bimap.getByKey(2)).toBeUndefined() + expect(bimap.getByValue('b')).toBeUndefined() + bimap.delete(1) + expect(bimap.getByKey(1)).toBeUndefined() + expect(bimap.getByValue('a')).toBeUndefined() + expect(bimap.size).toBe(1) + }) +})