1
1
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'
3
3
import debug from 'debug'
4
4
5
- import type { BaseNetworkConfig , ContentLookupResponse , FindContentMessage } from '../../index.js'
5
+ import type {
6
+ BaseNetworkConfig ,
7
+ ContentLookupResponse ,
8
+ FindContentMessage ,
9
+ INodeAddress ,
10
+ } from '../../index.js'
6
11
import {
12
+ BasicRadius ,
13
+ ClientInfoAndCapabilities ,
7
14
ContentMessageType ,
8
15
FoundContent ,
9
16
HistoricalSummariesBlockProof ,
17
+ HistoryRadius ,
18
+ MAX_PACKET_SIZE ,
10
19
MessageCodes ,
11
20
PingPongPayloadExtensions ,
12
21
PortalWireMessageType ,
13
22
RequestCode ,
14
23
decodeHistoryNetworkContentKey ,
15
24
decodeReceipts ,
25
+ encodeClientInfo ,
26
+ randUint16 ,
16
27
reassembleBlock ,
17
28
saveReceipts ,
18
29
shortId ,
@@ -24,6 +35,7 @@ import {
24
35
AccumulatorProofType ,
25
36
BlockHeaderWithProof ,
26
37
BlockNumberKey ,
38
+ EphemeralHeaderPayload ,
27
39
HistoricalRootsBlockProof ,
28
40
HistoryNetworkContentType ,
29
41
MERGE_BLOCK ,
@@ -32,6 +44,7 @@ import {
32
44
} from './types.js'
33
45
import {
34
46
getContentKey ,
47
+ getEphemeralHeaderDbKey ,
35
48
verifyPostCapellaHeaderProof ,
36
49
verifyPreCapellaHeaderProof ,
37
50
verifyPreMergeHeaderProof ,
@@ -46,7 +59,7 @@ export class HistoryNetwork extends BaseNetwork {
46
59
networkId : NetworkId . HistoryNetwork
47
60
networkName = 'HistoryNetwork'
48
61
logger : Debugger
49
-
62
+ public ephemeralHeaderIndex : Map < bigint , string > // Map of slot numbers to hashes
50
63
public blockHashIndex : Map < string , string >
51
64
constructor ( { client, db, radius, maxStorage } : BaseNetworkConfig ) {
52
65
super ( { client, networkId : NetworkId . HistoryNetwork , db, radius, maxStorage } )
@@ -58,6 +71,7 @@ export class HistoryNetwork extends BaseNetwork {
58
71
this . logger = debug ( this . enr . nodeId . slice ( 0 , 5 ) ) . extend ( 'Portal' ) . extend ( 'HistoryNetwork' )
59
72
this . routingTable . setLogger ( this . logger )
60
73
this . blockHashIndex = new Map ( )
74
+ this . ephemeralHeaderIndex = new Map ( )
61
75
}
62
76
63
77
public blockNumberToHash ( blockNumber : bigint ) : Uint8Array | undefined {
@@ -252,6 +266,37 @@ export class HistoryNetwork extends BaseNetwork {
252
266
return header . hash ( )
253
267
}
254
268
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
+ }
255
300
/**
256
301
* Send FINDCONTENT request for content corresponding to `key` to peer corresponding to `dstId`
257
302
* @param dstId node id of peer
@@ -260,6 +305,18 @@ export class HistoryNetwork extends BaseNetwork {
260
305
* @returns the value of the FOUNDCONTENT response or undefined
261
306
*/
262
307
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
+ }
263
320
this . portal . metrics ?. findContentMessagesSent . inc ( )
264
321
const findContentMsg : FindContentMessage = { contentKey : key }
265
322
const payload = PortalWireMessageType . serialize ( {
@@ -324,6 +381,68 @@ export class HistoryNetwork extends BaseNetwork {
324
381
}
325
382
}
326
383
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
+
327
446
/**
328
447
* Convenience method to add content for the History Network to the DB
329
448
* @param contentType - content type of the data item being stored
@@ -374,12 +493,61 @@ export class HistoryNetwork extends BaseNetwork {
374
493
}
375
494
break
376
495
}
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
+ }
377
543
}
378
544
379
545
this . emit ( 'ContentAdded' , contentKey , value )
380
546
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
+ }
383
551
}
384
552
this . logger (
385
553
`${ HistoryNetworkContentType [ contentType ] } added for ${
0 commit comments