github.com/ethereum-optimism/optimism@v1.7.2/op-node/p2p/gossip.go (about) 1 package p2p 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/sha256" 7 "encoding/binary" 8 "errors" 9 "fmt" 10 "sync" 11 "time" 12 13 "github.com/golang/snappy" 14 lru "github.com/hashicorp/golang-lru/v2" 15 pubsub "github.com/libp2p/go-libp2p-pubsub" 16 pb "github.com/libp2p/go-libp2p-pubsub/pb" 17 "github.com/libp2p/go-libp2p/core/host" 18 "github.com/libp2p/go-libp2p/core/peer" 19 20 "github.com/ethereum/go-ethereum/common" 21 "github.com/ethereum/go-ethereum/crypto" 22 "github.com/ethereum/go-ethereum/log" 23 24 "github.com/ethereum-optimism/optimism/op-node/rollup" 25 "github.com/ethereum-optimism/optimism/op-service/eth" 26 ) 27 28 const ( 29 // maxGossipSize limits the total size of gossip RPC containers as well as decompressed individual messages. 30 maxGossipSize = 10 * (1 << 20) 31 // minGossipSize is used to make sure that there is at least some data to validate the signature against. 32 minGossipSize = 66 33 maxOutboundQueue = 256 34 maxValidateQueue = 256 35 globalValidateThrottle = 512 36 gossipHeartbeat = 500 * time.Millisecond 37 // seenMessagesTTL limits the duration that message IDs are remembered for gossip deduplication purposes 38 // 130 * gossipHeartbeat 39 seenMessagesTTL = 130 * gossipHeartbeat 40 DefaultMeshD = 8 // topic stable mesh target count 41 DefaultMeshDlo = 6 // topic stable mesh low watermark 42 DefaultMeshDhi = 12 // topic stable mesh high watermark 43 DefaultMeshDlazy = 6 // gossip target 44 // peerScoreInspectFrequency is the frequency at which peer scores are inspected 45 peerScoreInspectFrequency = 15 * time.Second 46 ) 47 48 // Message domains, the msg id function uncompresses to keep data monomorphic, 49 // but invalid compressed data will need a unique different id. 50 51 var MessageDomainInvalidSnappy = [4]byte{0, 0, 0, 0} 52 var MessageDomainValidSnappy = [4]byte{1, 0, 0, 0} 53 54 type GossipSetupConfigurables interface { 55 PeerScoringParams() *ScoringParams 56 // ConfigureGossip creates configuration options to apply to the GossipSub setup 57 ConfigureGossip(rollupCfg *rollup.Config) []pubsub.Option 58 } 59 60 type GossipRuntimeConfig interface { 61 P2PSequencerAddress() common.Address 62 } 63 64 //go:generate mockery --name GossipMetricer 65 type GossipMetricer interface { 66 RecordGossipEvent(evType int32) 67 } 68 69 func blocksTopicV1(cfg *rollup.Config) string { 70 return fmt.Sprintf("/optimism/%s/0/blocks", cfg.L2ChainID.String()) 71 } 72 73 func blocksTopicV2(cfg *rollup.Config) string { 74 return fmt.Sprintf("/optimism/%s/1/blocks", cfg.L2ChainID.String()) 75 } 76 77 func blocksTopicV3(cfg *rollup.Config) string { 78 return fmt.Sprintf("/optimism/%s/2/blocks", cfg.L2ChainID.String()) 79 } 80 81 // BuildSubscriptionFilter builds a simple subscription filter, 82 // to help protect against peers spamming useless subscriptions. 83 func BuildSubscriptionFilter(cfg *rollup.Config) pubsub.SubscriptionFilter { 84 return pubsub.NewAllowlistSubscriptionFilter(blocksTopicV1(cfg), blocksTopicV2(cfg), blocksTopicV3(cfg)) // add more topics here in the future, if any. 85 } 86 87 var msgBufPool = sync.Pool{New: func() any { 88 // note: the topic validator concurrency is limited, so pool won't blow up, even with large pre-allocation. 89 x := make([]byte, 0, maxGossipSize) 90 return &x 91 }} 92 93 // BuildMsgIdFn builds a generic message ID function for gossipsub that can handle compressed payloads, 94 // mirroring the eth2 p2p gossip spec. 95 func BuildMsgIdFn(cfg *rollup.Config) pubsub.MsgIdFunction { 96 return func(pmsg *pb.Message) string { 97 valid := false 98 var data []byte 99 // If it's a valid compressed snappy data, then hash the uncompressed contents. 100 // The validator can throw away the message later when recognized as invalid, 101 // and the unique hash helps detect duplicates. 102 dLen, err := snappy.DecodedLen(pmsg.Data) 103 if err == nil && dLen <= maxGossipSize { 104 res := msgBufPool.Get().(*[]byte) 105 defer msgBufPool.Put(res) 106 if data, err = snappy.Decode((*res)[:0], pmsg.Data); err == nil { 107 *res = data // if we ended up growing the slice capacity, fine, keep the larger one. 108 valid = true 109 } 110 } 111 if data == nil { 112 data = pmsg.Data 113 } 114 h := sha256.New() 115 if valid { 116 h.Write(MessageDomainValidSnappy[:]) 117 } else { 118 h.Write(MessageDomainInvalidSnappy[:]) 119 } 120 // The chain ID is part of the gossip topic, making the msg id unique 121 topic := pmsg.GetTopic() 122 var topicLen [8]byte 123 binary.LittleEndian.PutUint64(topicLen[:], uint64(len(topic))) 124 h.Write(topicLen[:]) 125 h.Write([]byte(topic)) 126 h.Write(data) 127 // the message ID is shortened to save space, a lot of these may be gossiped. 128 return string(h.Sum(nil)[:20]) 129 } 130 } 131 132 func (p *Config) ConfigureGossip(rollupCfg *rollup.Config) []pubsub.Option { 133 params := BuildGlobalGossipParams(rollupCfg) 134 135 // override with CLI changes 136 params.D = p.MeshD 137 params.Dlo = p.MeshDLo 138 params.Dhi = p.MeshDHi 139 params.Dlazy = p.MeshDLazy 140 141 // in the future we may add more advanced options like scoring and PX / direct-mesh / episub 142 return []pubsub.Option{ 143 pubsub.WithGossipSubParams(params), 144 pubsub.WithFloodPublish(p.FloodPublish), 145 } 146 } 147 148 func BuildGlobalGossipParams(cfg *rollup.Config) pubsub.GossipSubParams { 149 params := pubsub.DefaultGossipSubParams() 150 params.D = DefaultMeshD // topic stable mesh target count 151 params.Dlo = DefaultMeshDlo // topic stable mesh low watermark 152 params.Dhi = DefaultMeshDhi // topic stable mesh high watermark 153 params.Dlazy = DefaultMeshDlazy // gossip target 154 params.HeartbeatInterval = gossipHeartbeat // interval of heartbeat 155 params.FanoutTTL = 24 * time.Second // ttl for fanout maps for topics we are not subscribed to but have published to 156 params.HistoryLength = 12 // number of windows to retain full messages in cache for IWANT responses 157 params.HistoryGossip = 3 // number of windows to gossip about 158 159 return params 160 } 161 162 // NewGossipSub configures a new pubsub instance with the specified parameters. 163 // PubSub uses a GossipSubRouter as it's router under the hood. 164 func NewGossipSub(p2pCtx context.Context, h host.Host, cfg *rollup.Config, gossipConf GossipSetupConfigurables, scorer Scorer, m GossipMetricer, log log.Logger) (*pubsub.PubSub, error) { 165 denyList, err := pubsub.NewTimeCachedBlacklist(30 * time.Second) 166 if err != nil { 167 return nil, err 168 } 169 gossipOpts := []pubsub.Option{ 170 pubsub.WithMaxMessageSize(maxGossipSize), 171 pubsub.WithMessageIdFn(BuildMsgIdFn(cfg)), 172 pubsub.WithNoAuthor(), 173 pubsub.WithMessageSignaturePolicy(pubsub.StrictNoSign), 174 pubsub.WithSubscriptionFilter(BuildSubscriptionFilter(cfg)), 175 pubsub.WithValidateQueueSize(maxValidateQueue), 176 pubsub.WithPeerOutboundQueueSize(maxOutboundQueue), 177 pubsub.WithValidateThrottle(globalValidateThrottle), 178 pubsub.WithSeenMessagesTTL(seenMessagesTTL), 179 pubsub.WithPeerExchange(false), 180 pubsub.WithBlacklist(denyList), 181 pubsub.WithEventTracer(&gossipTracer{m: m}), 182 } 183 gossipOpts = append(gossipOpts, ConfigurePeerScoring(gossipConf, scorer, log)...) 184 gossipOpts = append(gossipOpts, gossipConf.ConfigureGossip(cfg)...) 185 return pubsub.NewGossipSub(p2pCtx, h, gossipOpts...) 186 } 187 188 func validationResultString(v pubsub.ValidationResult) string { 189 switch v { 190 case pubsub.ValidationAccept: 191 return "ACCEPT" 192 case pubsub.ValidationIgnore: 193 return "IGNORE" 194 case pubsub.ValidationReject: 195 return "REJECT" 196 default: 197 return fmt.Sprintf("UNKNOWN_%d", v) 198 } 199 } 200 201 func logValidationResult(self peer.ID, msg string, log log.Logger, fn pubsub.ValidatorEx) pubsub.ValidatorEx { 202 return func(ctx context.Context, id peer.ID, message *pubsub.Message) pubsub.ValidationResult { 203 res := fn(ctx, id, message) 204 var src any 205 src = id 206 if id == self { 207 src = "self" 208 } 209 log.Debug(msg, "result", validationResultString(res), "from", src) 210 return res 211 } 212 } 213 214 func guardGossipValidator(log log.Logger, fn pubsub.ValidatorEx) pubsub.ValidatorEx { 215 return func(ctx context.Context, id peer.ID, message *pubsub.Message) (result pubsub.ValidationResult) { 216 defer func() { 217 if err := recover(); err != nil { 218 log.Error("gossip validation panic", "err", err, "peer", id) 219 result = pubsub.ValidationReject 220 } 221 }() 222 return fn(ctx, id, message) 223 } 224 } 225 226 type seenBlocks struct { 227 sync.Mutex 228 blockHashes []common.Hash 229 } 230 231 // hasSeen checks if the hash has been marked as seen, and how many have been seen. 232 func (sb *seenBlocks) hasSeen(h common.Hash) (count int, hasSeen bool) { 233 sb.Lock() 234 defer sb.Unlock() 235 for _, prev := range sb.blockHashes { 236 if prev == h { 237 return len(sb.blockHashes), true 238 } 239 } 240 return len(sb.blockHashes), false 241 } 242 243 // markSeen marks the block hash as seen 244 func (sb *seenBlocks) markSeen(h common.Hash) { 245 sb.Lock() 246 defer sb.Unlock() 247 sb.blockHashes = append(sb.blockHashes, h) 248 } 249 250 func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, blockVersion eth.BlockVersion) pubsub.ValidatorEx { 251 252 // Seen block hashes per block height 253 // uint64 -> *seenBlocks 254 blockHeightLRU, err := lru.New[uint64, *seenBlocks](1000) 255 if err != nil { 256 panic(fmt.Errorf("failed to set up block height LRU cache: %w", err)) 257 } 258 259 return func(ctx context.Context, id peer.ID, message *pubsub.Message) pubsub.ValidationResult { 260 // [REJECT] if the compression is not valid 261 outLen, err := snappy.DecodedLen(message.Data) 262 if err != nil { 263 log.Warn("invalid snappy compression length data", "err", err, "peer", id) 264 return pubsub.ValidationReject 265 } 266 if outLen > maxGossipSize { 267 log.Warn("possible snappy zip bomb, decoded length is too large", "decoded_length", outLen, "peer", id) 268 return pubsub.ValidationReject 269 } 270 if outLen < minGossipSize { 271 log.Warn("rejecting undersized gossip payload") 272 return pubsub.ValidationReject 273 } 274 275 res := msgBufPool.Get().(*[]byte) 276 defer msgBufPool.Put(res) 277 data, err := snappy.Decode((*res)[:0], message.Data) 278 if err != nil { 279 log.Warn("invalid snappy compression", "err", err, "peer", id) 280 return pubsub.ValidationReject 281 } 282 *res = data // if we ended up growing the slice capacity, fine, keep the larger one. 283 284 // message starts with compact-encoding secp256k1 encoded signature 285 signatureBytes, payloadBytes := data[:65], data[65:] 286 287 // [REJECT] if the signature by the sequencer is not valid 288 result := verifyBlockSignature(log, cfg, runCfg, id, signatureBytes, payloadBytes) 289 if result != pubsub.ValidationAccept { 290 return result 291 } 292 293 var envelope eth.ExecutionPayloadEnvelope 294 295 // [REJECT] if the block encoding is not valid 296 if blockVersion == eth.BlockV3 { 297 if err := envelope.UnmarshalSSZ(uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil { 298 log.Warn("invalid envelope payload", "err", err, "peer", id) 299 return pubsub.ValidationReject 300 } 301 } else { 302 var payload eth.ExecutionPayload 303 if err := payload.UnmarshalSSZ(blockVersion, uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil { 304 log.Warn("invalid execution payload", "err", err, "peer", id) 305 return pubsub.ValidationReject 306 } 307 envelope = eth.ExecutionPayloadEnvelope{ExecutionPayload: &payload} 308 } 309 310 payload := envelope.ExecutionPayload 311 312 // rounding down to seconds is fine here. 313 now := uint64(time.Now().Unix()) 314 315 // [REJECT] if the `payload.timestamp` is older than 60 seconds in the past 316 if uint64(payload.Timestamp) < now-60 { 317 log.Warn("payload is too old", "timestamp", uint64(payload.Timestamp)) 318 return pubsub.ValidationReject 319 } 320 321 // [REJECT] if the `payload.timestamp` is more than 5 seconds into the future 322 if uint64(payload.Timestamp) > now+5 { 323 log.Warn("payload is too new", "timestamp", uint64(payload.Timestamp)) 324 return pubsub.ValidationReject 325 } 326 327 // [REJECT] if the `block_hash` in the `payload` is not valid 328 if actual, ok := envelope.CheckBlockHash(); !ok { 329 log.Warn("payload has bad block hash", "bad_hash", payload.BlockHash.String(), "actual", actual.String()) 330 return pubsub.ValidationReject 331 } 332 333 // [REJECT] if a V1 Block has withdrawals 334 if !blockVersion.HasWithdrawals() && payload.Withdrawals != nil { 335 log.Warn("payload is on v1 topic, but has withdrawals", "bad_hash", payload.BlockHash.String()) 336 return pubsub.ValidationReject 337 } 338 339 // [REJECT] if a V2 Block does not have withdrawals 340 if blockVersion.HasWithdrawals() && payload.Withdrawals == nil { 341 log.Warn("payload is on v2/v3 topic, but does not have withdrawals", "bad_hash", payload.BlockHash.String()) 342 return pubsub.ValidationReject 343 } 344 345 // [REJECT] if a V2 Block has non-empty withdrawals 346 if blockVersion.HasWithdrawals() && len(*payload.Withdrawals) != 0 { 347 log.Warn("payload is on v2/v3 topic, but has non-empty withdrawals", "bad_hash", payload.BlockHash.String(), "withdrawal_count", len(*payload.Withdrawals)) 348 return pubsub.ValidationReject 349 } 350 351 // [REJECT] if the block is on a topic <= V2 and has a blob gas value set 352 if !blockVersion.HasBlobProperties() && payload.BlobGasUsed != nil { 353 log.Warn("payload is on v1/v2 topic, but has blob gas used", "bad_hash", payload.BlockHash.String()) 354 return pubsub.ValidationReject 355 } 356 357 // [REJECT] if the block is on a topic <= V2 and has an excess blob gas value set 358 if !blockVersion.HasBlobProperties() && payload.ExcessBlobGas != nil { 359 log.Warn("payload is on v1/v2 topic, but has excess blob gas", "bad_hash", payload.BlockHash.String()) 360 return pubsub.ValidationReject 361 } 362 363 if blockVersion.HasBlobProperties() { 364 // [REJECT] if the block is on a topic >= V3 and has a blob gas used value that is not zero 365 if payload.BlobGasUsed == nil || (payload.BlobGasUsed != nil && *payload.BlobGasUsed != 0) { 366 log.Warn("payload is on v3 topic, but has non-zero blob gas used", "bad_hash", payload.BlockHash.String(), "blob_gas_used", payload.BlobGasUsed) 367 return pubsub.ValidationReject 368 } 369 370 // [REJECT] if the block is on a topic >= V3 and has an excess blob gas value that is not zero 371 if payload.ExcessBlobGas == nil || (payload.ExcessBlobGas != nil && *payload.ExcessBlobGas != 0) { 372 log.Warn("payload is on v3 topic, but has non-zero excess blob gas", "bad_hash", payload.BlockHash.String(), "excess_blob_gas", payload.ExcessBlobGas) 373 return pubsub.ValidationReject 374 } 375 } 376 377 // [REJECT] if the block is on a topic >= V3 and the parent beacon block root is nil 378 if blockVersion.HasParentBeaconBlockRoot() && envelope.ParentBeaconBlockRoot == nil { 379 log.Warn("payload is on v3 topic, but has nil parent beacon block root", "bad_hash", payload.BlockHash.String()) 380 return pubsub.ValidationReject 381 } 382 383 seen, ok := blockHeightLRU.Get(uint64(payload.BlockNumber)) 384 if !ok { 385 seen = new(seenBlocks) 386 blockHeightLRU.Add(uint64(payload.BlockNumber), seen) 387 } 388 389 if count, hasSeen := seen.hasSeen(payload.BlockHash); count > 5 { 390 // [REJECT] if more than 5 blocks have been seen with the same block height 391 log.Warn("seen too many different blocks at same height", "height", payload.BlockNumber) 392 return pubsub.ValidationReject 393 } else if hasSeen { 394 // [IGNORE] if the block has already been seen 395 log.Warn("validated already seen message again") 396 return pubsub.ValidationIgnore 397 } 398 399 // mark it as seen. (note: with concurrent validation more than 5 blocks may be marked as seen still, 400 // but validator concurrency is limited anyway) 401 seen.markSeen(payload.BlockHash) 402 403 // remember the decoded payload for later usage in topic subscriber. 404 message.ValidatorData = &envelope 405 return pubsub.ValidationAccept 406 } 407 } 408 409 func verifyBlockSignature(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, id peer.ID, signatureBytes []byte, payloadBytes []byte) pubsub.ValidationResult { 410 signingHash, err := BlockSigningHash(cfg, payloadBytes) 411 if err != nil { 412 log.Warn("failed to compute block signing hash", "err", err, "peer", id) 413 return pubsub.ValidationReject 414 } 415 416 pub, err := crypto.SigToPub(signingHash[:], signatureBytes) 417 if err != nil { 418 log.Warn("invalid block signature", "err", err, "peer", id) 419 return pubsub.ValidationReject 420 } 421 addr := crypto.PubkeyToAddress(*pub) 422 423 // In the future we may load & validate block metadata before checking the signature. 424 // And then check the signer based on the metadata, to support e.g. multiple p2p signers at the same time. 425 // For now we only have one signer at a time and thus check the address directly. 426 // This means we may drop old payloads upon key rotation, 427 // but this can be recovered from like any other missed unsafe payload. 428 if expected := runCfg.P2PSequencerAddress(); expected == (common.Address{}) { 429 log.Warn("no configured p2p sequencer address, ignoring gossiped block", "peer", id, "addr", addr) 430 return pubsub.ValidationIgnore 431 } else if addr != expected { 432 log.Warn("unexpected block author", "err", err, "peer", id, "addr", addr, "expected", expected) 433 return pubsub.ValidationReject 434 } 435 return pubsub.ValidationAccept 436 } 437 438 type GossipIn interface { 439 OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *eth.ExecutionPayloadEnvelope) error 440 } 441 442 type GossipTopicInfo interface { 443 AllBlockTopicsPeers() []peer.ID 444 BlocksTopicV1Peers() []peer.ID 445 BlocksTopicV2Peers() []peer.ID 446 BlocksTopicV3Peers() []peer.ID 447 } 448 449 type GossipOut interface { 450 GossipTopicInfo 451 PublishL2Payload(ctx context.Context, msg *eth.ExecutionPayloadEnvelope, signer Signer) error 452 Close() error 453 } 454 455 type blockTopic struct { 456 // blocks topic, main handle on block gossip 457 topic *pubsub.Topic 458 // block events handler, to be cancelled before closing the blocks topic. 459 events *pubsub.TopicEventHandler 460 // block subscriptions, to be cancelled before closing blocks topic. 461 sub *pubsub.Subscription 462 } 463 464 func (bt *blockTopic) Close() error { 465 bt.events.Cancel() 466 bt.sub.Cancel() 467 return bt.topic.Close() 468 } 469 470 type publisher struct { 471 log log.Logger 472 cfg *rollup.Config 473 474 // p2pCancel cancels the downstream gossip event-handling functions, independent of the sources. 475 // A closed gossip event source (event handler or subscription) does not stop any open event iteration, 476 // thus we have to stop it ourselves this way. 477 p2pCancel context.CancelFunc 478 479 blocksV1 *blockTopic 480 blocksV2 *blockTopic 481 blocksV3 *blockTopic 482 483 runCfg GossipRuntimeConfig 484 } 485 486 var _ GossipOut = (*publisher)(nil) 487 488 func combinePeers(allPeers ...[]peer.ID) []peer.ID { 489 var seen = make(map[peer.ID]bool) 490 var res []peer.ID 491 for _, peers := range allPeers { 492 for _, p := range peers { 493 if _, ok := seen[p]; ok { 494 continue 495 } 496 res = append(res, p) 497 seen[p] = true 498 } 499 } 500 return res 501 } 502 503 func (p *publisher) AllBlockTopicsPeers() []peer.ID { 504 return combinePeers(p.BlocksTopicV1Peers(), p.BlocksTopicV2Peers(), p.BlocksTopicV3Peers()) 505 } 506 507 func (p *publisher) BlocksTopicV1Peers() []peer.ID { 508 return p.blocksV1.topic.ListPeers() 509 } 510 511 func (p *publisher) BlocksTopicV2Peers() []peer.ID { 512 return p.blocksV2.topic.ListPeers() 513 } 514 515 func (p *publisher) BlocksTopicV3Peers() []peer.ID { 516 return p.blocksV3.topic.ListPeers() 517 } 518 519 func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, signer Signer) error { 520 res := msgBufPool.Get().(*[]byte) 521 buf := bytes.NewBuffer((*res)[:0]) 522 defer func() { 523 *res = buf.Bytes() 524 defer msgBufPool.Put(res) 525 }() 526 527 buf.Write(make([]byte, 65)) 528 529 if envelope.ParentBeaconBlockRoot != nil { 530 if _, err := envelope.MarshalSSZ(buf); err != nil { 531 return fmt.Errorf("failed to encoded execution payload envelope to publish: %w", err) 532 } 533 } else { 534 if _, err := envelope.ExecutionPayload.MarshalSSZ(buf); err != nil { 535 return fmt.Errorf("failed to encoded execution payload to publish: %w", err) 536 } 537 } 538 539 data := buf.Bytes() 540 payloadData := data[65:] 541 sig, err := signer.Sign(ctx, SigningDomainBlocksV1, p.cfg.L2ChainID, payloadData) 542 if err != nil { 543 return fmt.Errorf("failed to sign execution payload with signer: %w", err) 544 } 545 copy(data[:65], sig[:]) 546 547 // compress the full message 548 // This also copies the data, freeing up the original buffer to go back into the pool 549 out := snappy.Encode(nil, data) 550 551 if p.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) { 552 return p.blocksV3.topic.Publish(ctx, out) 553 } else if p.cfg.IsCanyon(uint64(envelope.ExecutionPayload.Timestamp)) { 554 return p.blocksV2.topic.Publish(ctx, out) 555 } else { 556 return p.blocksV1.topic.Publish(ctx, out) 557 } 558 } 559 560 func (p *publisher) Close() error { 561 p.p2pCancel() 562 e1 := p.blocksV1.Close() 563 e2 := p.blocksV2.Close() 564 return errors.Join(e1, e2) 565 } 566 567 func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, error) { 568 p2pCtx, p2pCancel := context.WithCancel(context.Background()) 569 570 v1Logger := log.New("topic", "blocksV1") 571 blocksV1Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv1", v1Logger, BuildBlocksValidator(v1Logger, cfg, runCfg, eth.BlockV1))) 572 blocksV1, err := newBlockTopic(p2pCtx, blocksTopicV1(cfg), ps, v1Logger, gossipIn, blocksV1Validator) 573 if err != nil { 574 p2pCancel() 575 return nil, fmt.Errorf("failed to setup blocks v1 p2p: %w", err) 576 } 577 578 v2Logger := log.New("topic", "blocksV2") 579 blocksV2Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv2", v2Logger, BuildBlocksValidator(v2Logger, cfg, runCfg, eth.BlockV2))) 580 blocksV2, err := newBlockTopic(p2pCtx, blocksTopicV2(cfg), ps, v2Logger, gossipIn, blocksV2Validator) 581 if err != nil { 582 p2pCancel() 583 return nil, fmt.Errorf("failed to setup blocks v2 p2p: %w", err) 584 } 585 586 v3Logger := log.New("topic", "blocksV3") 587 blocksV3Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv3", v3Logger, BuildBlocksValidator(v3Logger, cfg, runCfg, eth.BlockV3))) 588 blocksV3, err := newBlockTopic(p2pCtx, blocksTopicV3(cfg), ps, v3Logger, gossipIn, blocksV3Validator) 589 if err != nil { 590 p2pCancel() 591 return nil, fmt.Errorf("failed to setup blocks v3 p2p: %w", err) 592 } 593 594 return &publisher{ 595 log: log, 596 cfg: cfg, 597 p2pCancel: p2pCancel, 598 blocksV1: blocksV1, 599 blocksV2: blocksV2, 600 blocksV3: blocksV3, 601 runCfg: runCfg, 602 }, nil 603 } 604 605 func newBlockTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, log log.Logger, gossipIn GossipIn, validator pubsub.ValidatorEx) (*blockTopic, error) { 606 err := ps.RegisterTopicValidator(topicId, 607 validator, 608 pubsub.WithValidatorTimeout(3*time.Second), 609 pubsub.WithValidatorConcurrency(4)) 610 611 if err != nil { 612 return nil, fmt.Errorf("failed to register gossip topic: %w", err) 613 } 614 615 blocksTopic, err := ps.Join(topicId) 616 if err != nil { 617 return nil, fmt.Errorf("failed to join gossip topic: %w", err) 618 } 619 620 blocksTopicEvents, err := blocksTopic.EventHandler() 621 if err != nil { 622 return nil, fmt.Errorf("failed to create blocks gossip topic handler: %w", err) 623 } 624 625 go LogTopicEvents(ctx, log, blocksTopicEvents) 626 627 subscription, err := blocksTopic.Subscribe() 628 if err != nil { 629 err = errors.Join(err, blocksTopic.Close()) 630 return nil, fmt.Errorf("failed to subscribe to blocks gossip topic: %w", err) 631 } 632 633 subscriber := MakeSubscriber(log, BlocksHandler(gossipIn.OnUnsafeL2Payload)) 634 go subscriber(ctx, subscription) 635 636 return &blockTopic{ 637 topic: blocksTopic, 638 events: blocksTopicEvents, 639 sub: subscription, 640 }, nil 641 } 642 643 type TopicSubscriber func(ctx context.Context, sub *pubsub.Subscription) 644 type MessageHandler func(ctx context.Context, from peer.ID, msg any) error 645 646 func BlocksHandler(onBlock func(ctx context.Context, from peer.ID, msg *eth.ExecutionPayloadEnvelope) error) MessageHandler { 647 return func(ctx context.Context, from peer.ID, msg any) error { 648 payload, ok := msg.(*eth.ExecutionPayloadEnvelope) 649 if !ok { 650 return fmt.Errorf("expected topic validator to parse and validate data into execution payload, but got %T", msg) 651 } 652 return onBlock(ctx, from, payload) 653 } 654 } 655 656 func MakeSubscriber(log log.Logger, msgHandler MessageHandler) TopicSubscriber { 657 return func(ctx context.Context, sub *pubsub.Subscription) { 658 topicLog := log.New("topic", sub.Topic()) 659 for { 660 msg, err := sub.Next(ctx) 661 if err != nil { // ctx was closed, or subscription was closed 662 topicLog.Debug("stopped subscriber") 663 return 664 } 665 if msg.ValidatorData == nil { 666 topicLog.Error("gossip message with no data", "from", msg.ReceivedFrom) 667 continue 668 } 669 if err := msgHandler(ctx, msg.ReceivedFrom, msg.ValidatorData); err != nil { 670 topicLog.Error("failed to process gossip message", "err", err) 671 } 672 } 673 } 674 } 675 676 func LogTopicEvents(ctx context.Context, log log.Logger, evHandler *pubsub.TopicEventHandler) { 677 for { 678 ev, err := evHandler.NextPeerEvent(ctx) 679 if err != nil { 680 return // ctx closed 681 } 682 switch ev.Type { 683 case pubsub.PeerJoin: 684 log.Debug("peer joined topic", "peer", ev.Peer) 685 case pubsub.PeerLeave: 686 log.Debug("peer left topic", "peer", ev.Peer) 687 default: 688 log.Warn("unrecognized topic event", "ev", ev) 689 } 690 } 691 } 692 693 type gossipTracer struct { 694 m GossipMetricer 695 } 696 697 func (g *gossipTracer) Trace(evt *pb.TraceEvent) { 698 if g.m != nil { 699 g.m.RecordGossipEvent(int32(*evt.Type)) 700 } 701 }