Repository navigation
Expand file tree
/
Copy pathhost.go
More file actions
323 lines (256 loc) · 9.87 KB
/
Copy pathhost.go
File metadata and controls
323 lines (256 loc) · 9.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// Package lp2p implements auxiliary functions for go-libp2p integration in CometBFT.
// The name is chosen to avoid conflicts with the p2p package.
package lp2p
import (
"context"
"fmt"
"time"
"github.com/cometbft/cometbft/config"
cmcrypto "github.com/cometbft/cometbft/crypto"
"github.com/cometbft/cometbft/libs/log"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/connmgr"
"github.com/libp2p/go-libp2p/core/control"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
"github.com/libp2p/go-libp2p/p2p/protocol/ping"
quic "github.com/libp2p/go-libp2p/p2p/transport/quic"
multiaddr "github.com/multiformats/go-multiaddr"
)
// Host is a wrapper around the libp2p host.
// Note that host should NOT be responsible for high-level peer management
// as it's Switch's responsibility.
type Host struct {
host.Host
config config.LibP2PConfig
// bootstrapPeers are initial peers specified in the address book
bootstrapPeers map[peer.ID]BootstrapPeer
logger log.Logger
peerFailureHandlers []func(id peer.ID, err error)
}
// BootstrapPeer initial peers to connect to
type BootstrapPeer struct {
AddrInfo peer.AddrInfo
Private bool
Persistent bool
Unconditional bool
}
// TransportQUIC quic transport.
// @see https://docs.libp2p.io/concepts/transports/quic
const TransportQUIC = "quic-v1"
// NewHost Host constructor.
func NewHost(config *config.P2PConfig, nodeKey cmcrypto.PrivKey, logger log.Logger) (*Host, error) {
if !config.LibP2PEnabled() {
return nil, fmt.Errorf("libp2p is disabled")
}
if err := config.LibP2PConfig.ValidateBasic(); err != nil {
return nil, fmt.Errorf("invalid libp2p config: %w", err)
}
privateKey, err := PrivateKeyFromCosmosKey(nodeKey)
if err != nil {
return nil, fmt.Errorf("failed to convert private key to libp2p: %w", err)
}
listenAddr, err := AddressToMultiAddr(config.ListenAddress, TransportQUIC)
if err != nil {
return nil, fmt.Errorf("failed to convert %q to multiaddr: %w", config.ListenAddress, err)
}
bootstrapPeers, err := BootstrapPeersFromConfig(config.LibP2PConfig)
switch {
case err != nil:
return nil, fmt.Errorf("failed to decode bootstrap peers: %w", err)
case len(bootstrapPeers) == 0:
logger.Info("No bootstrap peers provided in the config")
}
// host will be set later
connGater, connGaterEnabled := ConnectionGaterFromConfig(config.LibP2PConfig, nil)
resourceManager, _, err := ResourceManagerFromConfig(config.LibP2PConfig)
if err != nil {
return nil, fmt.Errorf("failed to create resource manager: %w", err)
}
// todo: add support for libp2p.BandwidthReporter()
opts := []libp2p.Option{
libp2p.Identity(privateKey),
libp2p.ListenAddrs(listenAddr),
libp2p.UserAgent("cometbft"),
libp2p.Ping(true),
libp2p.Transport(quic.NewTransport),
libp2p.ResourceManager(resourceManager),
}
if connGaterEnabled {
opts = append(opts, libp2p.ConnectionGater(connGater))
}
// We listen on `listenAddr` but advertise `externalAddr` to peers
if config.ExternalAddress != "" {
externalAddr, err := AddressToMultiAddr(config.ExternalAddress, TransportQUIC)
if err != nil {
return nil, fmt.Errorf("failed to convert %q to multiaddr: %w", config.ExternalAddress, err)
}
opts = append(opts, withAddressFactory(externalAddr))
}
host, err := libp2p.New(opts...)
if err != nil {
return nil, fmt.Errorf("failed to create libp2p host: %w", err)
}
h := &Host{
Host: host,
config: config.LibP2PConfig,
bootstrapPeers: bootstrapPeers,
logger: logger,
}
if connGaterEnabled {
connGater.SetHost(h)
}
return h, nil
}
func (h *Host) AddrInfo() peer.AddrInfo {
return peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}
}
func (h *Host) BootstrapPeers() map[peer.ID]BootstrapPeer {
return h.bootstrapPeers
}
func (h *Host) BootstrapPeer(id peer.ID) (BootstrapPeer, bool) {
bp, ok := h.bootstrapPeers[id]
return bp, ok
}
func (h *Host) Logger() log.Logger {
return h.logger
}
// Ping pings peers and logs RTT latency (blocking)
// Keep in might that ping service might be disabled on the counterparty's side.
func (h *Host) Ping(ctx context.Context, addrInfo peer.AddrInfo) (time.Duration, error) {
res := <-ping.Ping(ctx, h, addrInfo.ID)
return res.RTT, res.Error
}
func (h *Host) AddPeerFailureHandler(handler func(id peer.ID, err error)) {
h.peerFailureHandlers = append(h.peerFailureHandlers, handler)
}
// EmitPeerFailure emits a peer failure event to all registered handlers.
// This semantic is over host.eventBus for simplicity.
func (h *Host) EmitPeerFailure(id peer.ID, err error) {
for _, handler := range h.peerFailureHandlers {
go handler(id, err)
}
}
func BootstrapPeersFromConfig(config config.LibP2PConfig) (map[peer.ID]BootstrapPeer, error) {
peers := make(map[peer.ID]BootstrapPeer, len(config.BootstrapPeers))
for _, bp := range config.BootstrapPeers {
addr, err := AddrInfoFromHostAndID(bp.Host, bp.ID)
if err != nil {
return nil, fmt.Errorf("[%s, %s]: %w", bp.Host, bp.ID, err)
}
if _, ok := peers[addr.ID]; ok {
continue
}
peers[addr.ID] = BootstrapPeer{
AddrInfo: addr,
Private: bp.Private,
Persistent: bp.Persistent,
Unconditional: bp.Unconditional,
}
}
return peers, nil
}
// systemConnsPerPeer is the per-peer connection budget used to size the
// system-wide connection limit in custom limits mode.
const systemConnsPerPeer = 4
// ResourceManagerFromConfig creates a resource manager from the given config.
func ResourceManagerFromConfig(cfg config.LibP2PConfig) (network.ResourceManager, rcmgr.Limiter, error) {
if cfg.Limits.Mode == config.LibP2PLimitsModeDisabled {
return &network.NullResourceManager{}, nil, nil
}
// this is what lib-p2p does by default:
// mem limit: 1/8th of total memory, max 128MB, min 1GB (see defaults.AutoScale())
defaults := rcmgr.DefaultLimits
// cap limits for default lib-p2p protocols (identity, ping, ...)
libp2p.SetDefaultServiceLimits(&defaults)
if cfg.Limits.Mode == config.LibP2PLimitsModeDefault {
limiter := rcmgr.NewFixedLimiter(defaults.AutoScale())
mgr, err := rcmgr.NewResourceManager(limiter)
return mgr, limiter, err
}
if cfg.Limits.Mode == config.LibP2PLimitsModeCustom {
var (
partialDefaults = defaults.AutoScale().ToPartialLimitConfig()
limits = rcmgr.InfiniteLimits.ToPartialLimitConfig()
maxPeers = rcmgr.LimitVal(cfg.Limits.MaxPeers)
maxPeerStreams = rcmgr.LimitVal(cfg.Limits.MaxPeerStreams)
)
// 1. copy defaults for built-in services/protocols
limits.Service = partialDefaults.Service
limits.ServicePeer = partialDefaults.ServicePeer
limits.Protocol = partialDefaults.Protocol
limits.ProtocolPeer = partialDefaults.ProtocolPeer
// 2. also copy sane default conns for peers
limits.PeerDefault.Conns = partialDefaults.PeerDefault.Conns
limits.PeerDefault.ConnsInbound = partialDefaults.PeerDefault.ConnsInbound
limits.PeerDefault.ConnsOutbound = partialDefaults.PeerDefault.ConnsOutbound
// 2.1 limit max system connections to (max peers * conns per peer).
// Peers normally hold a single multiplexed connection; the small
// constant leaves room for a reconnect while the old one drains.
limits.System.Conns = maxPeers * systemConnsPerPeer
// 3. set max streams
// https://github.com/libp2p/go-libp2p/blob/da810a1/p2p/host/resource-manager/scope.go#L168
limits.PeerDefault.Streams = maxPeerStreams
limits.PeerDefault.StreamsInbound = maxPeerStreams
limits.PeerDefault.StreamsOutbound = maxPeerStreams
limiter := rcmgr.NewFixedLimiter(limits.Build(rcmgr.InfiniteLimits))
mgr, err := rcmgr.NewResourceManager(limiter)
return mgr, limiter, err
}
return nil, nil, fmt.Errorf("unknown limits mode: %q", cfg.Limits.Mode)
}
// ConnGater limits the number of simultaneously connected peers.
// It is only enabled when `lp2p.limits.mode = "custom"` and uses
// `lp2p.limits.max_peers` as the cap.
//
// The host is injected after host creation because libp2p requires the
// connection gater option during `libp2p.New(...)`, before the host exists.
type ConnGater struct {
host *Host
maxPeers int
}
var _ connmgr.ConnectionGater = (*ConnGater)(nil)
// ConnectionGaterFromConfig creates a connection gater from the given config or returns false if disabled.
func ConnectionGaterFromConfig(cfg config.LibP2PConfig, host *Host) (*ConnGater, bool) {
if cfg.Limits.Mode != config.LibP2PLimitsModeCustom {
return nil, false
}
return &ConnGater{
host: host,
maxPeers: cfg.Limits.MaxPeers,
}, true
}
// SetHost sets the host for the connection gater. The host is injected after creation
// because libp2p requires the connection gater option during libp2p.New, before the host exists.
func (c *ConnGater) SetHost(host *Host) { c.host = host }
// InterceptAccept is called when a peer attempts to connect. It returns false to reject the connection
// if the peer count has reached max_peers.
func (c *ConnGater) InterceptAccept(network.ConnMultiaddrs) bool {
return c.allowMorePeers("caller", "InterceptAccept")
}
func (c *ConnGater) InterceptAddrDial(pid peer.ID, _ multiaddr.Multiaddr) bool {
return c.allowMorePeers("caller", "InterceptAddrDial", "peer_id", pid.String())
}
func (c *ConnGater) InterceptPeerDial(pid peer.ID) bool {
return c.allowMorePeers("caller", "InterceptPeerDial", "peer_id", pid.String())
}
func (c *ConnGater) InterceptSecured(network.Direction, peer.ID, network.ConnMultiaddrs) bool {
return true
}
func (c *ConnGater) InterceptUpgraded(network.Conn) (allow bool, reason control.DisconnectReason) {
return true, 0
}
func (c *ConnGater) allowMorePeers(labels ...any) bool {
if c.host == nil {
return false
}
current := len(c.host.Network().Peers())
if current < c.maxPeers {
return true
}
labels = append(labels, "current_peers", current, "max_peers", c.maxPeers)
c.host.logger.Info("Rejecting peer due to max peers limit", labels...)
return false
}