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  }