Skip to content

Commit a0cca03

Browse files
acolytec3ScottyPoi
andauthored
Implement support for ephemeral headers (#733)
* Remove union type * fix tests and utils * fix more tests * remove unused method * Add ephemeral headers type * Validate and store ephemeral headers * Fixes and first test * add ephemeral header index * Support current ephemeral header count in pingPong payload * override sendFindContent * remove beacon findContent override * fix test * Update packages/portalnetwork/src/networks/history/history.ts Co-authored-by: Scotty <[email protected]> --------- Co-authored-by: Scotty <[email protected]>
1 parent 11d0efe commit a0cca03

File tree

8 files changed

+1190
-50
lines changed

8 files changed

+1190
-50
lines changed

packages/portalnetwork/src/networks/beacon/beacon.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import type { Debugger } from 'debug'
5151
import type { AcceptMessage, FindContentMessage, OfferMessage } from '../../wire/types.js'
5252
import type { ContentLookupResponse } from '../types.js'
5353
import type { BeaconChainNetworkConfig, HistoricalSummaries, LightClientForkName } from './types.js'
54-
import type { INodeAddress } from '../../index.js';
54+
import type { INodeAddress } from '../../index.js'
5555

5656
export class BeaconLightClientNetwork extends BaseNetwork {
5757
networkId: NetworkId.BeaconChainNetwork
@@ -561,7 +561,6 @@ export class BeaconLightClientNetwork extends BaseNetwork {
561561
protected override handleFindContent = async (
562562
src: INodeAddress,
563563
requestId: bigint,
564-
network: Uint8Array,
565564
decodedContentMessage: FindContentMessage,
566565
) => {
567566
this.portal.metrics?.contentMessagesSent.inc()
@@ -853,7 +852,7 @@ export class BeaconLightClientNetwork extends BaseNetwork {
853852
return msg.contentKeys
854853
}
855854
} catch (err: any) {
856-
this.logger(`Error sending to ${shortId(enr.nodeId)} - ${err.message}`)
855+
this.logger(`Error sending to ${shortId(enr.nodeId)} - ${err.message}`)
857856
}
858857
}
859858
}

packages/portalnetwork/src/networks/history/history.ts

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import { Block, BlockHeader } from '@ethereumjs/block'
2-
import { bytesToHex, bytesToInt, equalsBytes, hexToBytes } from '@ethereumjs/util'
2+
import { bytesToHex, bytesToInt, concatBytes, equalsBytes, hexToBytes } from '@ethereumjs/util'
33
import debug from 'debug'
44

5-
import type { BaseNetworkConfig, ContentLookupResponse, FindContentMessage } from '../../index.js'
5+
import type {
6+
BaseNetworkConfig,
7+
ContentLookupResponse,
8+
FindContentMessage,
9+
INodeAddress,
10+
} from '../../index.js'
611
import {
12+
BasicRadius,
13+
ClientInfoAndCapabilities,
714
ContentMessageType,
815
FoundContent,
916
HistoricalSummariesBlockProof,
17+
HistoryRadius,
18+
MAX_PACKET_SIZE,
1019
MessageCodes,
1120
PingPongPayloadExtensions,
1221
PortalWireMessageType,
1322
RequestCode,
1423
decodeHistoryNetworkContentKey,
1524
decodeReceipts,
25+
encodeClientInfo,
26+
randUint16,
1627
reassembleBlock,
1728
saveReceipts,
1829
shortId,
@@ -24,6 +35,7 @@ import {
2435
AccumulatorProofType,
2536
BlockHeaderWithProof,
2637
BlockNumberKey,
38+
EphemeralHeaderPayload,
2739
HistoricalRootsBlockProof,
2840
HistoryNetworkContentType,
2941
MERGE_BLOCK,
@@ -32,6 +44,7 @@ import {
3244
} from './types.js'
3345
import {
3446
getContentKey,
47+
getEphemeralHeaderDbKey,
3548
verifyPostCapellaHeaderProof,
3649
verifyPreCapellaHeaderProof,
3750
verifyPreMergeHeaderProof,
@@ -46,7 +59,7 @@ export class HistoryNetwork extends BaseNetwork {
4659
networkId: NetworkId.HistoryNetwork
4760
networkName = 'HistoryNetwork'
4861
logger: Debugger
49-
62+
public ephemeralHeaderIndex: Map<bigint, string> // Map of slot numbers to hashes
5063
public blockHashIndex: Map<string, string>
5164
constructor({ client, db, radius, maxStorage }: BaseNetworkConfig) {
5265
super({ client, networkId: NetworkId.HistoryNetwork, db, radius, maxStorage })
@@ -58,6 +71,7 @@ export class HistoryNetwork extends BaseNetwork {
5871
this.logger = debug(this.enr.nodeId.slice(0, 5)).extend('Portal').extend('HistoryNetwork')
5972
this.routingTable.setLogger(this.logger)
6073
this.blockHashIndex = new Map()
74+
this.ephemeralHeaderIndex = new Map()
6175
}
6276

6377
public blockNumberToHash(blockNumber: bigint): Uint8Array | undefined {
@@ -252,6 +266,37 @@ export class HistoryNetwork extends BaseNetwork {
252266
return header.hash()
253267
}
254268

269+
public override pingPongPayload(extensionType: number) {
270+
let payload: Uint8Array
271+
switch (extensionType) {
272+
case PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES: {
273+
payload = ClientInfoAndCapabilities.serialize({
274+
ClientInfo: encodeClientInfo(this.portal.clientInfo),
275+
DataRadius: this.nodeRadius,
276+
Capabilities: this.capabilities,
277+
})
278+
break
279+
}
280+
case PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD: {
281+
payload = BasicRadius.serialize({ dataRadius: this.nodeRadius })
282+
break
283+
}
284+
case PingPongPayloadExtensions.HISTORY_RADIUS_PAYLOAD: {
285+
if (this.networkId !== NetworkId.HistoryNetwork) {
286+
throw new Error('HISTORY_RADIUS extension not supported on this network')
287+
}
288+
payload = HistoryRadius.serialize({
289+
dataRadius: this.nodeRadius,
290+
ephemeralHeadersCount: this.ephemeralHeaderIndex.size,
291+
})
292+
break
293+
}
294+
default: {
295+
throw new Error(`Unsupported PING extension type: ${extensionType}`)
296+
}
297+
}
298+
return payload
299+
}
255300
/**
256301
* Send FINDCONTENT request for content corresponding to `key` to peer corresponding to `dstId`
257302
* @param dstId node id of peer
@@ -260,6 +305,18 @@ export class HistoryNetwork extends BaseNetwork {
260305
* @returns the value of the FOUNDCONTENT response or undefined
261306
*/
262307
public sendFindContent = async (enr: ENR, key: Uint8Array) => {
308+
if (key[0] === HistoryNetworkContentType.EphemeralHeader) {
309+
const beacon = this.portal.network()['0x500c']
310+
if (
311+
beacon === undefined ||
312+
beacon.lightClient?.status === RunStatusCode.uninitialized ||
313+
beacon.lightClient?.status === RunStatusCode.stopped
314+
) {
315+
const errorMessage = 'Cannot verify ephemeral headers when beacon network is not running'
316+
this.logger.extend('FINDCONTENT')(errorMessage)
317+
throw new Error(errorMessage)
318+
}
319+
}
263320
this.portal.metrics?.findContentMessagesSent.inc()
264321
const findContentMsg: FindContentMessage = { contentKey: key }
265322
const payload = PortalWireMessageType.serialize({
@@ -324,6 +381,68 @@ export class HistoryNetwork extends BaseNetwork {
324381
}
325382
}
326383

384+
protected override handleFindContent = async (
385+
src: INodeAddress,
386+
requestId: bigint,
387+
decodedContentMessage: FindContentMessage,
388+
) => {
389+
this.portal.metrics?.contentMessagesSent.inc()
390+
391+
this.logger(
392+
`Received FindContent request for contentKey: ${bytesToHex(
393+
decodedContentMessage.contentKey,
394+
)}`,
395+
)
396+
397+
// TODO: Add specific support for retrieving ephemeral headers
398+
const value = await this.findContentLocally(decodedContentMessage.contentKey)
399+
if (!value) {
400+
await this.enrResponse(decodedContentMessage.contentKey, src, requestId)
401+
} else if (value instanceof Uint8Array && value.length < MAX_PACKET_SIZE) {
402+
this.logger(
403+
'Found value for requested content ' +
404+
bytesToHex(decodedContentMessage.contentKey) +
405+
' ' +
406+
bytesToHex(value.slice(0, 10)) +
407+
`...`,
408+
)
409+
const payload = ContentMessageType.serialize({
410+
selector: 1,
411+
value,
412+
})
413+
this.logger.extend('CONTENT')(`Sending requested content to ${src.nodeId}`)
414+
await this.sendResponse(
415+
src,
416+
requestId,
417+
concatBytes(Uint8Array.from([MessageCodes.CONTENT]), payload),
418+
)
419+
} else {
420+
this.logger.extend('FOUNDCONTENT')(
421+
'Found value for requested content. Larger than 1 packet. uTP stream needed.',
422+
)
423+
const _id = randUint16()
424+
const enr = this.findEnr(src.nodeId) ?? src
425+
await this.handleNewRequest({
426+
networkId: this.networkId,
427+
contentKeys: [decodedContentMessage.contentKey],
428+
enr,
429+
connectionId: _id,
430+
requestCode: RequestCode.FOUNDCONTENT_WRITE,
431+
contents: value,
432+
})
433+
434+
const id = new Uint8Array(2)
435+
new DataView(id.buffer).setUint16(0, _id, false)
436+
this.logger.extend('FOUNDCONTENT')(`Sent message with CONNECTION ID: ${_id}.`)
437+
const payload = ContentMessageType.serialize({ selector: FoundContent.UTP, value: id })
438+
await this.sendResponse(
439+
src,
440+
requestId,
441+
concatBytes(Uint8Array.from([MessageCodes.CONTENT]), payload),
442+
)
443+
}
444+
}
445+
327446
/**
328447
* Convenience method to add content for the History Network to the DB
329448
* @param contentType - content type of the data item being stored
@@ -374,12 +493,61 @@ export class HistoryNetwork extends BaseNetwork {
374493
}
375494
break
376495
}
496+
497+
case HistoryNetworkContentType.EphemeralHeader: {
498+
const payload = EphemeralHeaderPayload.deserialize(value)
499+
try {
500+
// Verify first header matches requested header
501+
const firstHeader = BlockHeader.fromRLPSerializedHeader(payload[0], { setHardfork: true })
502+
const requestedHeaderHash = decodeHistoryNetworkContentKey(contentKey)
503+
.keyOpt as Uint8Array
504+
if (!equalsBytes(firstHeader.hash(), requestedHeaderHash)) {
505+
// TODO: Should we ban/mark down the score of peers who send junk payload?
506+
const errorMessage = `invalid ephemeral header payload; requested ${bytesToHex(requestedHeaderHash)}, got ${bytesToHex(firstHeader.hash())}`
507+
this.logger(errorMessage)
508+
throw new Error(errorMessage)
509+
}
510+
const hashKey = getEphemeralHeaderDbKey(firstHeader.hash())
511+
await this.put(hashKey, bytesToHex(payload[0]))
512+
// Index ephemeral header by block number
513+
this.ephemeralHeaderIndex.set(firstHeader.number, bytesToHex(firstHeader.hash()))
514+
let prevHeader = firstHeader
515+
// Should get maximum of 256 headers
516+
// TODO: Should we check this and ban/mark down the score of peers who violate this rule?
517+
for (const header of payload.slice(1, 256)) {
518+
const ancestorHeader = BlockHeader.fromRLPSerializedHeader(header, {
519+
setHardfork: true,
520+
})
521+
if (equalsBytes(prevHeader.parentHash, ancestorHeader.hash())) {
522+
// Verify that ancestor header matches parent hash of previous header
523+
const hashKey = getEphemeralHeaderDbKey(ancestorHeader.hash())
524+
await this.put(hashKey, bytesToHex(header))
525+
// Index ephemeral header by block number
526+
this.ephemeralHeaderIndex.set(
527+
ancestorHeader.number,
528+
bytesToHex(ancestorHeader.hash()),
529+
)
530+
prevHeader = ancestorHeader
531+
} else {
532+
const errorMessage = `invalid ephemeral header payload; expected parent hash ${bytesToHex(ancestorHeader.parentHash)} but got ${bytesToHex(prevHeader.hash())}`
533+
this.logger(errorMessage)
534+
throw new Error(errorMessage)
535+
}
536+
}
537+
break
538+
} catch (err: any) {
539+
this.logger(`Error validating ephemeral header: ${err.message}`)
540+
return
541+
}
542+
}
377543
}
378544

379545
this.emit('ContentAdded', contentKey, value)
380546
if (this.routingTable.values().length > 0) {
381-
// Gossip new content to network
382-
this.gossipManager.add(contentKey)
547+
if (contentType !== HistoryNetworkContentType.EphemeralHeader) {
548+
// Gossip new content to network except for ephemeral headers
549+
this.gossipManager.add(contentKey)
550+
}
383551
}
384552
this.logger(
385553
`${HistoryNetworkContentType[contentType]} added for ${

packages/portalnetwork/src/networks/history/types.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ContainerType,
55
ListCompositeType,
66
UintBigintType,
7+
UintNumberType,
78
VectorCompositeType,
89
} from '@chainsafe/ssz'
910
import { MAX_WITHDRAWALS_PER_PAYLOAD } from '@lodestar/params'
@@ -35,7 +36,7 @@ export enum HistoryNetworkContentType {
3536
BlockBody = 1,
3637
Receipt = 2,
3738
BlockHeaderByNumber = 3,
38-
HeaderProof = 4,
39+
EphemeralHeader = 4,
3940
}
4041
export enum HistoryNetworkRetrievalMechanism {
4142
BlockHeaderByHash = 0,
@@ -98,6 +99,9 @@ export type SszProof = {
9899
leaf: HashRoot
99100
witnesses: Witnesses
100101
}
102+
103+
export const BlockHeader = new ByteListType(MAX_HEADER_LENGTH)
104+
101105
export type HeaderRecord = {
102106
blockHash: HashRoot
103107
totalDifficulty: TotalDifficulty
@@ -209,3 +213,15 @@ export const BlockHeaderWithProof = new ContainerType({
209213
header: new ByteListType(MAX_HEADER_LENGTH),
210214
proof: new ByteListType(MAX_HEADER_PROOF_LENGTH),
211215
})
216+
217+
/** Ephemeral header types */
218+
export const EphemeralHeaderKey = new ContainerType({
219+
blockHash: Bytes32Type,
220+
ancestorCount: new UintNumberType(1),
221+
})
222+
223+
export const MAX_EPHEMERAL_HEADERS_PAYLOAD = 255 // the max number of headers that can be sent in an ephemeral headers payload
224+
export const EphemeralHeaderPayload = new ListCompositeType(
225+
BlockHeader,
226+
MAX_EPHEMERAL_HEADERS_PAYLOAD,
227+
)

0 commit comments

Comments
 (0)