github.com/ethereum-optimism/optimism@v1.7.2/op-node/p2p/node.go (about) 1 package p2p 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "strconv" 9 "time" 10 11 "github.com/hashicorp/go-multierror" 12 pubsub "github.com/libp2p/go-libp2p-pubsub" 13 "github.com/libp2p/go-libp2p/core/connmgr" 14 "github.com/libp2p/go-libp2p/core/host" 15 p2pmetrics "github.com/libp2p/go-libp2p/core/metrics" 16 "github.com/libp2p/go-libp2p/core/network" 17 "github.com/libp2p/go-libp2p/core/peer" 18 ma "github.com/multiformats/go-multiaddr" 19 manet "github.com/multiformats/go-multiaddr/net" 20 21 "github.com/ethereum/go-ethereum/log" 22 "github.com/ethereum/go-ethereum/p2p/discover" 23 "github.com/ethereum/go-ethereum/p2p/enode" 24 25 "github.com/ethereum-optimism/optimism/op-node/metrics" 26 "github.com/ethereum-optimism/optimism/op-node/p2p/gating" 27 "github.com/ethereum-optimism/optimism/op-node/p2p/monitor" 28 "github.com/ethereum-optimism/optimism/op-node/p2p/store" 29 "github.com/ethereum-optimism/optimism/op-node/rollup" 30 "github.com/ethereum-optimism/optimism/op-service/clock" 31 "github.com/ethereum-optimism/optimism/op-service/eth" 32 ) 33 34 // NodeP2P is a p2p node, which can be used to gossip messages. 35 type NodeP2P struct { 36 host host.Host // p2p host (optional, may be nil) 37 gater gating.BlockingConnectionGater // p2p gater, to ban/unban peers with, may be nil even with p2p enabled 38 scorer Scorer // writes score-updates to the peerstore and keeps metrics of score changes 39 connMgr connmgr.ConnManager // p2p conn manager, to keep a reliable number of peers, may be nil even with p2p enabled 40 peerMonitor *monitor.PeerMonitor // peer monitor to disconnect bad peers, may be nil even with p2p enabled 41 store store.ExtendedPeerstore // peerstore of host, with extra bindings for scoring and banning 42 appScorer ApplicationScorer 43 log log.Logger 44 // the below components are all optional, and may be nil. They require the host to not be nil. 45 dv5Local *enode.LocalNode // p2p discovery identity 46 dv5Udp *discover.UDPv5 // p2p discovery service 47 gs *pubsub.PubSub // p2p gossip router 48 gsOut GossipOut // p2p gossip application interface for publishing 49 syncCl *SyncClient 50 syncSrv *ReqRespServer 51 } 52 53 // NewNodeP2P creates a new p2p node, and returns a reference to it. If the p2p is disabled, it returns nil. 54 // If metrics are configured, a bandwidth monitor will be spawned in a goroutine. 55 func NewNodeP2P(resourcesCtx context.Context, rollupCfg *rollup.Config, log log.Logger, setup SetupP2P, gossipIn GossipIn, l2Chain L2Chain, runCfg GossipRuntimeConfig, metrics metrics.Metricer, elSyncEnabled bool) (*NodeP2P, error) { 56 if setup == nil { 57 return nil, errors.New("p2p node cannot be created without setup") 58 } 59 var n NodeP2P 60 if err := n.init(resourcesCtx, rollupCfg, log, setup, gossipIn, l2Chain, runCfg, metrics, elSyncEnabled); err != nil { 61 closeErr := n.Close() 62 if closeErr != nil { 63 log.Error("failed to close p2p after starting with err", "closeErr", closeErr, "err", err) 64 } 65 return nil, err 66 } 67 if n.host == nil { 68 return nil, nil 69 } 70 return &n, nil 71 } 72 73 func (n *NodeP2P) init(resourcesCtx context.Context, rollupCfg *rollup.Config, log log.Logger, setup SetupP2P, gossipIn GossipIn, l2Chain L2Chain, runCfg GossipRuntimeConfig, metrics metrics.Metricer, elSyncEnabled bool) error { 74 bwc := p2pmetrics.NewBandwidthCounter() 75 76 n.log = log 77 78 var err error 79 // nil if disabled. 80 n.host, err = setup.Host(log, bwc, metrics) 81 if err != nil { 82 if n.dv5Udp != nil { 83 n.dv5Udp.Close() 84 } 85 return fmt.Errorf("failed to start p2p host: %w", err) 86 } 87 88 // TODO(CLI-4016): host is not optional, NodeP2P as a whole is. This if statement is wrong 89 if n.host != nil { 90 // Enable extra features, if any. During testing we don't setup the most advanced host all the time. 91 if extra, ok := n.host.(ExtraHostFeatures); ok { 92 n.gater = extra.ConnectionGater() 93 n.connMgr = extra.ConnectionManager() 94 } 95 eps, ok := n.host.Peerstore().(store.ExtendedPeerstore) 96 if !ok { 97 return fmt.Errorf("cannot init without extended peerstore: %w", err) 98 } 99 n.store = eps 100 scoreParams := setup.PeerScoringParams() 101 102 if scoreParams != nil { 103 n.appScorer = newPeerApplicationScorer(resourcesCtx, log, clock.SystemClock, &scoreParams.ApplicationScoring, eps, n.host.Network().Peers) 104 } else { 105 n.appScorer = &NoopApplicationScorer{} 106 } 107 // Activate the P2P req-resp sync if enabled by feature-flag. 108 if setup.ReqRespSyncEnabled() && !elSyncEnabled { 109 n.syncCl = NewSyncClient(log, rollupCfg, n.host.NewStream, gossipIn.OnUnsafeL2Payload, metrics, n.appScorer) 110 n.host.Network().Notify(&network.NotifyBundle{ 111 ConnectedF: func(nw network.Network, conn network.Conn) { 112 n.syncCl.AddPeer(conn.RemotePeer()) 113 }, 114 DisconnectedF: func(nw network.Network, conn network.Conn) { 115 // only when no connection is available, we can remove the peer 116 if nw.Connectedness(conn.RemotePeer()) == network.NotConnected { 117 n.syncCl.RemovePeer(conn.RemotePeer()) 118 } 119 }, 120 }) 121 n.syncCl.Start() 122 // the host may already be connected to peers, add them all to the sync client 123 for _, peerID := range n.host.Network().Peers() { 124 n.syncCl.AddPeer(peerID) 125 } 126 if l2Chain != nil { // Only enable serving side of req-resp sync if we have a data-source, to make minimal P2P testing easy 127 n.syncSrv = NewReqRespServer(rollupCfg, l2Chain, metrics) 128 // register the sync protocol with libp2p host 129 payloadByNumber := MakeStreamHandler(resourcesCtx, log.New("serve", "payloads_by_number"), n.syncSrv.HandleSyncRequest) 130 n.host.SetStreamHandler(PayloadByNumberProtocolID(rollupCfg.L2ChainID), payloadByNumber) 131 } 132 } 133 n.scorer = NewScorer(rollupCfg, eps, metrics, n.appScorer, log) 134 // notify of any new connections/streams/etc. 135 n.host.Network().Notify(NewNetworkNotifier(log, metrics)) 136 // note: the IDDelta functionality was removed from libP2P, and no longer needs to be explicitly disabled. 137 n.gs, err = NewGossipSub(resourcesCtx, n.host, rollupCfg, setup, n.scorer, metrics, log) 138 if err != nil { 139 return fmt.Errorf("failed to start gossipsub router: %w", err) 140 } 141 n.gsOut, err = JoinGossip(n.host.ID(), n.gs, log, rollupCfg, runCfg, gossipIn) 142 if err != nil { 143 return fmt.Errorf("failed to join blocks gossip topic: %w", err) 144 } 145 log.Info("started p2p host", "addrs", n.host.Addrs(), "peerID", n.host.ID().String()) 146 147 tcpPort, err := FindActiveTCPPort(n.host) 148 if err != nil { 149 log.Warn("failed to find what TCP port p2p is binded to", "err", err) 150 } 151 152 // All nil if disabled. 153 n.dv5Local, n.dv5Udp, err = setup.Discovery(log.New("p2p", "discv5"), rollupCfg, tcpPort) 154 if err != nil { 155 return fmt.Errorf("failed to start discv5: %w", err) 156 } 157 158 if metrics != nil { 159 go metrics.RecordBandwidth(resourcesCtx, bwc) 160 } 161 162 if setup.BanPeers() { 163 n.peerMonitor = monitor.NewPeerMonitor(resourcesCtx, log, clock.SystemClock, n, setup.BanThreshold(), setup.BanDuration()) 164 n.peerMonitor.Start() 165 } 166 n.appScorer.start() 167 } 168 return nil 169 } 170 171 func (n *NodeP2P) AltSyncEnabled() bool { 172 return n.syncCl != nil 173 } 174 175 func (n *NodeP2P) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error { 176 if !n.AltSyncEnabled() { 177 return fmt.Errorf("cannot request range %s - %s, req-resp sync is not enabled", start, end) 178 } 179 return n.syncCl.RequestL2Range(ctx, start, end) 180 } 181 182 func (n *NodeP2P) Host() host.Host { 183 return n.host 184 } 185 186 func (n *NodeP2P) Dv5Local() *enode.LocalNode { 187 return n.dv5Local 188 } 189 190 func (n *NodeP2P) Dv5Udp() *discover.UDPv5 { 191 return n.dv5Udp 192 } 193 194 func (n *NodeP2P) GossipSub() *pubsub.PubSub { 195 return n.gs 196 } 197 198 func (n *NodeP2P) GossipOut() GossipOut { 199 return n.gsOut 200 } 201 202 func (n *NodeP2P) ConnectionGater() gating.BlockingConnectionGater { 203 return n.gater 204 } 205 206 func (n *NodeP2P) ConnectionManager() connmgr.ConnManager { 207 return n.connMgr 208 } 209 210 func (n *NodeP2P) Peers() []peer.ID { 211 return n.host.Network().Peers() 212 } 213 214 func (n *NodeP2P) GetPeerScore(id peer.ID) (float64, error) { 215 return n.store.GetPeerScore(id) 216 } 217 218 func (n *NodeP2P) IsStatic(id peer.ID) bool { 219 return n.connMgr != nil && n.connMgr.IsProtected(id, staticPeerTag) 220 } 221 222 func (n *NodeP2P) BanPeer(id peer.ID, expiration time.Time) error { 223 if err := n.store.SetPeerBanExpiration(id, expiration); err != nil { 224 return fmt.Errorf("failed to set peer ban expiry: %w", err) 225 } 226 if err := n.host.Network().ClosePeer(id); err != nil { 227 return fmt.Errorf("failed to close peer connection: %w", err) 228 } 229 return nil 230 } 231 232 func (n *NodeP2P) BanIP(ip net.IP, expiration time.Time) error { 233 if err := n.store.SetIPBanExpiration(ip, expiration); err != nil { 234 return fmt.Errorf("failed to set IP ban expiry: %w", err) 235 } 236 // kick all peers that match this IP 237 for _, conn := range n.host.Network().Conns() { 238 addr := conn.RemoteMultiaddr() 239 remoteIP, err := manet.ToIP(addr) 240 if err != nil { 241 continue 242 } 243 if remoteIP.Equal(ip) { 244 if err := conn.Close(); err != nil { 245 n.log.Error("failed to close connection to peer with banned IP", "peer", conn.RemotePeer(), "ip", ip) 246 } 247 } 248 } 249 return nil 250 } 251 252 func (n *NodeP2P) Close() error { 253 var result *multierror.Error 254 if n.peerMonitor != nil { 255 n.peerMonitor.Stop() 256 } 257 if n.dv5Udp != nil { 258 n.dv5Udp.Close() 259 } 260 if n.gsOut != nil { 261 if err := n.gsOut.Close(); err != nil { 262 result = multierror.Append(result, fmt.Errorf("failed to close gossip cleanly: %w", err)) 263 } 264 } 265 if n.host != nil { 266 if err := n.host.Close(); err != nil { 267 result = multierror.Append(result, fmt.Errorf("failed to close p2p host cleanly: %w", err)) 268 } 269 if n.syncCl != nil { 270 if err := n.syncCl.Close(); err != nil { 271 result = multierror.Append(result, fmt.Errorf("failed to close p2p sync client cleanly: %w", err)) 272 } 273 } 274 } 275 if n.appScorer != nil { 276 n.appScorer.stop() 277 } 278 return result.ErrorOrNil() 279 } 280 281 func FindActiveTCPPort(h host.Host) (uint16, error) { 282 var tcpPort uint16 283 for _, addr := range h.Addrs() { 284 tcpPortStr, err := addr.ValueForProtocol(ma.P_TCP) 285 if err != nil { 286 continue 287 } 288 v, err := strconv.ParseUint(tcpPortStr, 10, 16) 289 if err != nil { 290 continue 291 } 292 tcpPort = uint16(v) 293 break 294 } 295 return tcpPort, nil 296 }