network

package
v0.99.1-pre Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2022 License: MIT Imports: 28 Imported by: 1

Documentation

Index

Constants

View Source
const CompressionMinSize = 1024

CompressionMinSize is the lower bound to apply compression.

Variables

This section is empty.

Functions

This section is empty.

Types

type AddressWithCapabilities added in v0.90.0

type AddressWithCapabilities struct {
	Address      string
	Capabilities capability.Capabilities
}

AddressWithCapabilities represents a node address with its capabilities.

type Blockqueuer added in v0.98.1

type Blockqueuer interface {
	AddBlock(block *block.Block) error
	AddHeaders(...*block.Header) error
	BlockHeight() uint32
}

Blockqueuer is an interface for a block queue.

type CommandType

type CommandType byte

CommandType represents the type of a message command.

const (
	// Handshaking.
	CMDVersion CommandType = 0x00
	CMDVerack  CommandType = 0x01

	// Connectivity.
	CMDGetAddr CommandType = 0x10
	CMDAddr    CommandType = 0x11
	CMDPing    CommandType = 0x18
	CMDPong    CommandType = 0x19

	// Synchronization.
	CMDGetHeaders       CommandType = 0x20
	CMDHeaders          CommandType = 0x21
	CMDGetBlocks        CommandType = 0x24
	CMDMempool          CommandType = 0x25
	CMDInv              CommandType = 0x27
	CMDGetData          CommandType = 0x28
	CMDGetBlockByIndex  CommandType = 0x29
	CMDNotFound         CommandType = 0x2a
	CMDTX                           = CommandType(payload.TXType)
	CMDBlock                        = CommandType(payload.BlockType)
	CMDExtensible                   = CommandType(payload.ExtensibleType)
	CMDP2PNotaryRequest             = CommandType(payload.P2PNotaryRequestType)
	CMDGetMPTData       CommandType = 0x51 // 0x5.. commands are used for extensions (P2PNotary, state exchange cmds)
	CMDMPTData          CommandType = 0x52
	CMDReject           CommandType = 0x2f

	// SPV protocol.
	CMDFilterLoad  CommandType = 0x30
	CMDFilterAdd   CommandType = 0x31
	CMDFilterClear CommandType = 0x32
	CMDMerkleBlock CommandType = 0x38

	// Others.
	CMDAlert CommandType = 0x40
)

Valid protocol commands used to send between nodes.

func (CommandType) String added in v0.90.0

func (i CommandType) String() string

type DefaultDiscovery

type DefaultDiscovery struct {
	// contains filtered or unexported fields
}

DefaultDiscovery default implementation of the Discoverer interface.

func NewDefaultDiscovery

func NewDefaultDiscovery(addrs []string, dt time.Duration, ts Transporter) *DefaultDiscovery

NewDefaultDiscovery returns a new DefaultDiscovery.

func (*DefaultDiscovery) BackFill

func (d *DefaultDiscovery) BackFill(addrs ...string)

BackFill implements the Discoverer interface and will backfill the pool with the given addresses.

func (*DefaultDiscovery) BadPeers

func (d *DefaultDiscovery) BadPeers() []string

BadPeers returns all addresses of bad addrs.

func (*DefaultDiscovery) Close

func (d *DefaultDiscovery) Close()

Close stops discoverer pool processing, which makes the discoverer almost useless.

func (*DefaultDiscovery) GoodPeers

func (d *DefaultDiscovery) GoodPeers() []AddressWithCapabilities

GoodPeers returns all addresses of known good peers (that at least once succeeded handshaking with us).

func (*DefaultDiscovery) PoolCount

func (d *DefaultDiscovery) PoolCount() int

PoolCount returns the number of the available node addresses.

func (*DefaultDiscovery) RegisterBadAddr

func (d *DefaultDiscovery) RegisterBadAddr(addr string)

RegisterBadAddr registers the given address as a bad address.

func (*DefaultDiscovery) RegisterConnectedAddr

func (d *DefaultDiscovery) RegisterConnectedAddr(addr string)

RegisterConnectedAddr tells discoverer that the given address is now connected.

func (*DefaultDiscovery) RegisterGoodAddr

func (d *DefaultDiscovery) RegisterGoodAddr(s string, c capability.Capabilities)

RegisterGoodAddr registers a known good connected address that has passed handshake successfully.

func (*DefaultDiscovery) RequestRemote

func (d *DefaultDiscovery) RequestRemote(n int)

RequestRemote tries to establish a connection with n nodes.

func (*DefaultDiscovery) UnconnectedPeers

func (d *DefaultDiscovery) UnconnectedPeers() []string

UnconnectedPeers returns all addresses of unconnected addrs.

func (*DefaultDiscovery) UnregisterConnectedAddr

func (d *DefaultDiscovery) UnregisterConnectedAddr(s string)

UnregisterConnectedAddr tells the discoverer that this address is no longer connected, but it is still considered a good one.

type Discoverer

type Discoverer interface {
	BackFill(...string)
	Close()
	PoolCount() int
	RequestRemote(int)
	RegisterBadAddr(string)
	RegisterGoodAddr(string, capability.Capabilities)
	RegisterConnectedAddr(string)
	UnregisterConnectedAddr(string)
	UnconnectedPeers() []string
	BadPeers() []string
	GoodPeers() []AddressWithCapabilities
}

Discoverer is an interface that is responsible for maintaining a healthy connection pool.

type Ledger added in v0.98.1

type Ledger interface {
	extpool.Ledger
	mempool.Feer
	Blockqueuer
	GetBlock(hash util.Uint256) (*block.Block, error)
	GetConfig() config.ProtocolConfiguration
	GetHeader(hash util.Uint256) (*block.Header, error)
	GetHeaderHash(int) util.Uint256
	GetMaxVerificationGAS() int64
	GetMemPool() *mempool.Pool
	GetNotaryBalance(acc util.Uint160) *big.Int
	GetNotaryContractScriptHash() util.Uint160
	GetNotaryDepositExpiration(acc util.Uint160) uint32
	GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
	HasBlock(util.Uint256) bool
	HeaderHeight() uint32
	P2PSigExtensionsEnabled() bool
	PoolTx(t *transaction.Transaction, pools ...*mempool.Pool) error
	PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(t *transaction.Transaction, data interface{}) error) error
	RegisterPostBlock(f func(func(*transaction.Transaction, *mempool.Pool, bool) bool, *mempool.Pool, *block.Block))
	SubscribeForBlocks(ch chan<- *block.Block)
	UnsubscribeFromBlocks(ch chan<- *block.Block)
}

Ledger is everything Server needs from the blockchain.

type Message

type Message struct {
	// Flags that represents whether a message is compressed.
	// 0 for None, 1 for Compressed.
	Flags MessageFlag
	// Command is a byte command code.
	Command CommandType

	// Payload send with the message.
	Payload payload.Payload

	// StateRootInHeader specifies if the state root is included in the block header.
	// This is needed for correct decoding.
	StateRootInHeader bool
	// contains filtered or unexported fields
}

Message is a complete message sent between nodes.

func NewMessage

func NewMessage(cmd CommandType, p payload.Payload) *Message

NewMessage returns a new message with the given payload.

func (*Message) Bytes

func (m *Message) Bytes() ([]byte, error)

Bytes serializes a Message into the new allocated buffer and returns it.

func (*Message) Decode

func (m *Message) Decode(br *io.BinReader) error

Decode decodes a Message from the given reader.

func (*Message) Encode

func (m *Message) Encode(br *io.BinWriter) error

Encode encodes a Message to any given BinWriter.

type MessageFlag added in v0.90.0

type MessageFlag byte

MessageFlag represents compression level of a message payload.

const (
	Compressed MessageFlag = 1 << iota
	None       MessageFlag = 0
)

Possible message flags.

type NotaryFeer added in v0.92.0

type NotaryFeer struct {
	// contains filtered or unexported fields
}

NotaryFeer implements mempool.Feer interface for Notary balance handling.

func NewNotaryFeer added in v0.92.0

func NewNotaryFeer(bc Ledger) NotaryFeer

NewNotaryFeer returns new NotaryFeer instance.

func (NotaryFeer) BlockHeight added in v0.92.0

func (f NotaryFeer) BlockHeight() uint32

BlockHeight implements mempool.Feer interface.

func (NotaryFeer) FeePerByte added in v0.92.0

func (f NotaryFeer) FeePerByte() int64

FeePerByte implements mempool.Feer interface.

func (NotaryFeer) GetUtilityTokenBalance added in v0.92.0

func (f NotaryFeer) GetUtilityTokenBalance(acc util.Uint160) *big.Int

GetUtilityTokenBalance implements mempool.Feer interface.

func (NotaryFeer) P2PSigExtensionsEnabled added in v0.92.0

func (f NotaryFeer) P2PSigExtensionsEnabled() bool

P2PSigExtensionsEnabled implements mempool.Feer interface.

type Peer

type Peer interface {
	// RemoteAddr returns the remote address that we're connected to now.
	RemoteAddr() net.Addr
	// PeerAddr returns the remote address that should be used to establish
	// a new connection to the node. It can differ from the RemoteAddr
	// address in case the remote node is a client and its current
	// connection port is different from the one the other node should use
	// to connect to it. It's only valid after the handshake is completed.
	// Before that, it returns the same address as RemoteAddr.
	PeerAddr() net.Addr
	Disconnect(error)

	// EnqueueMessage is a temporary wrapper that sends a message via
	// EnqueuePacket if there is no error in serializing it.
	EnqueueMessage(*Message) error

	// EnqueuePacket is a blocking packet enqueuer, it doesn't return until
	// it puts the given packet into the queue. It accepts a slice of bytes that
	// can be shared with other queues (so that message marshalling can be
	// done once for all peers). It does nothing if the peer has not yet
	// completed handshaking.
	EnqueuePacket(bool, []byte) error

	// EnqueueP2PMessage is a temporary wrapper that sends a message via
	// EnqueueP2PPacket if there is no error in serializing it.
	EnqueueP2PMessage(*Message) error

	// EnqueueP2PPacket is a blocking packet enqueuer, it doesn't return until
	// it puts the given packet into the queue. It accepts a slice of bytes that
	// can be shared with other queues (so that message marshalling can be
	// done once for all peers). It does nothing if the peer has not yet
	// completed handshaking. This queue is intended to be used for unicast
	// peer to peer communication that is more important than broadcasts
	// (handled by EnqueuePacket) but less important than high-priority
	// messages (handled by EnqueueHPPacket).
	EnqueueP2PPacket([]byte) error

	// EnqueueHPPacket is a blocking high priority packet enqueuer, it
	// doesn't return until it puts the given packet into the high-priority
	// queue.
	EnqueueHPPacket(bool, []byte) error
	Version() *payload.Version
	LastBlockIndex() uint32
	Handshaked() bool
	IsFullNode() bool

	// SendPing enqueues a ping message to be sent to the peer and does
	// appropriate protocol handling like timeouts and outstanding pings
	// management.
	SendPing(*Message) error
	// SendVersion checks handshake status and sends a version message to
	// the peer.
	SendVersion() error
	SendVersionAck(*Message) error
	// StartProtocol is a goroutine to be run after the handshake. It
	// implements basic peer-related protocol handling.
	StartProtocol()
	HandleVersion(*payload.Version) error
	HandleVersionAck() error

	// HandlePing checks ping contents against Peer's state and updates it.
	HandlePing(ping *payload.Ping) error

	// HandlePong checks pong contents against Peer's state and updates it.
	HandlePong(pong *payload.Ping) error

	// AddGetAddrSent is to inform local peer context that a getaddr command
	// is sent. The decision to send getaddr is server-wide, but it needs to be
	// accounted for in peer's context, thus this method.
	AddGetAddrSent()

	// CanProcessAddr checks whether an addr command is expected to come from
	// this peer and can be processed.
	CanProcessAddr() bool
}

Peer represents a network node neo-go is connected to.

type Server

type Server struct {
	// ServerConfig holds the Server configuration.
	ServerConfig
	// contains filtered or unexported fields
}

Server represents the local Node in the network. Its transport could be of any kind.

func NewServer

func NewServer(config ServerConfig, chain Ledger, stSync StateSync, log *zap.Logger) (*Server, error)

NewServer returns a new Server, initialized with the given configuration.

func (*Server) AddExtensibleHPService added in v0.98.1

func (s *Server) AddExtensibleHPService(svc Service, category string, handler func(*payload.Extensible) error, txCallback func(*transaction.Transaction))

AddExtensibleHPService registers a high-priority service that handles an extensible payload of some kind.

func (*Server) AddExtensibleService added in v0.98.1

func (s *Server) AddExtensibleService(svc Service, category string, handler func(*payload.Extensible) error)

AddExtensibleService register a service that handles an extensible payload of some kind.

func (*Server) AddService added in v0.98.1

func (s *Server) AddService(svc Service)

AddService allows to add a service to be started/stopped by Server.

func (*Server) BadPeers

func (s *Server) BadPeers() []string

BadPeers returns a list of peers that are flagged as "bad" peers.

func (*Server) BroadcastExtensible added in v0.98.1

func (s *Server) BroadcastExtensible(p *payload.Extensible)

BroadcastExtensible add a locally-generated Extensible payload to the pool and advertises it to peers.

func (*Server) ConnectedPeers

func (s *Server) ConnectedPeers() []string

ConnectedPeers returns a list of currently connected peers.

func (*Server) GetNotaryPool added in v0.98.1

func (s *Server) GetNotaryPool() *mempool.Pool

GetNotaryPool allows to retrieve notary pool, if it's configured.

func (*Server) HandshakedPeersCount

func (s *Server) HandshakedPeersCount() int

HandshakedPeersCount returns the number of the connected peers which have already performed handshake.

func (*Server) ID

func (s *Server) ID() uint32

ID returns the servers ID.

func (*Server) IsInSync

func (s *Server) IsInSync() bool

IsInSync answers the question of whether the server is in sync with the network or not (at least how the server itself sees it). The server operates with the data that it has, the number of peers (that has to be more than minimum number) and the height of these peers (our chain has to be not lower than 2/3 of our peers have). Ideally, we would check for the highest of the peers, but the problem is that they can lie to us and send whatever height they want to.

func (*Server) PeerCount

func (s *Server) PeerCount() int

PeerCount returns the number of the currently connected peers.

func (*Server) Port added in v0.90.0

func (s *Server) Port() (uint16, error)

Port returns a server port that should be used in P2P version exchange. In case `AnnouncedPort` is set in the server.Config, the announced node port will be returned (e.g. consider the node running behind NAT). If `AnnouncedPort` isn't set, the port returned may still differs from that of server.Config.

func (*Server) RelayP2PNotaryRequest added in v0.93.0

func (s *Server) RelayP2PNotaryRequest(r *payload.P2PNotaryRequest) error

RelayP2PNotaryRequest adds the given request to the pool and relays. It does not check P2PSigExtensions enabled.

func (*Server) RelayTxn

func (s *Server) RelayTxn(t *transaction.Transaction) error

RelayTxn a new transaction to the local node and the connected peers. Reference: the method OnRelay in C#: https://github.com/neo-project/neo/blob/master/neo/Network/P2P/LocalNode.cs#L159

func (*Server) RequestTx added in v0.98.1

func (s *Server) RequestTx(hashes ...util.Uint256)

RequestTx asks for the given transactions from Server peers using GetData message.

func (*Server) Shutdown

func (s *Server) Shutdown()

Shutdown disconnects all peers and stops listening.

func (*Server) Start

func (s *Server) Start(errChan chan error)

Start will start the server and its underlying transport.

func (*Server) SubscribeForNotaryRequests added in v0.95.2

func (s *Server) SubscribeForNotaryRequests(ch chan<- mempoolevent.Event)

SubscribeForNotaryRequests adds the given channel to a notary request event broadcasting, so when a new P2PNotaryRequest is received or an existing P2PNotaryRequest is removed from the pool you'll receive it via this channel. Make sure it's read from regularly as not reading these events might affect other Server functions. Ensure that P2PSigExtensions are enabled before calling this method.

func (*Server) UnconnectedPeers

func (s *Server) UnconnectedPeers() []string

UnconnectedPeers returns a list of peers that are in the discovery peer list but are not connected to the server.

func (*Server) UnsubscribeFromNotaryRequests added in v0.95.2

func (s *Server) UnsubscribeFromNotaryRequests(ch chan<- mempoolevent.Event)

UnsubscribeFromNotaryRequests unsubscribes the given channel from notary request notifications, you can close it afterwards. Passing non-subscribed channel is a no-op. Ensure that P2PSigExtensions are enabled before calling this method.

type ServerConfig

type ServerConfig struct {
	// MinPeers is the minimum number of peers for normal operation.
	// When a node has less than this number of peers, it tries to
	// connect with some new ones.
	MinPeers int

	// AttemptConnPeers is the number of connection to try to
	// establish when the connection count drops below the MinPeers
	// value.
	AttemptConnPeers int

	// MaxPeers is the maximum number of peers that can
	// be connected to the server.
	MaxPeers int

	// The user agent of the server.
	UserAgent string

	// Address. Example: "127.0.0.1".
	Address string

	// AnnouncedPort is an announced node port for P2P version exchange.
	AnnouncedPort uint16

	// Port is the actual node port it is bound to. Example: 20332.
	Port uint16

	// The network mode the server will operate on.
	// ModePrivNet docker private network.
	// ModeTestNet NEO test network.
	// ModeMainNet NEO main network.
	Net netmode.Magic

	// Relay determines whether the server is forwarding its inventory.
	Relay bool

	// Seeds is a list of initial nodes used to establish connectivity.
	Seeds []string

	// Maximum duration a single dial may take.
	DialTimeout time.Duration

	// The duration between protocol ticks with each connected peer.
	// When this is 0, the default interval of 5 seconds will be used.
	ProtoTickInterval time.Duration

	// Interval used in pinging mechanism for syncing blocks.
	PingInterval time.Duration
	// Time to wait for pong(response for sent ping request).
	PingTimeout time.Duration

	// Level of the internal logger.
	LogLevel zapcore.Level

	// Wallet is a wallet configuration.
	Wallet *config.Wallet

	// TimePerBlock is an interval which should pass between two successive blocks.
	TimePerBlock time.Duration

	// OracleCfg is oracle module configuration.
	OracleCfg config.OracleConfiguration

	// P2PNotaryCfg is notary module configuration.
	P2PNotaryCfg config.P2PNotary

	// StateRootCfg is stateroot module configuration.
	StateRootCfg config.StateRoot

	// ExtensiblePoolSize is the size of the pool for extensible payloads from a single sender.
	ExtensiblePoolSize int
}

ServerConfig holds the server configuration.

func NewServerConfig

func NewServerConfig(cfg config.Config) ServerConfig

NewServerConfig creates a new ServerConfig struct using the main applications config.

type Service added in v0.98.1

type Service interface {
	Name() string
	Start()
	Shutdown()
}

Service is a service abstraction (oracle, state root, consensus, etc).

type StateSync added in v0.98.1

type StateSync interface {
	AddMPTNodes([][]byte) error
	Blockqueuer
	Init(currChainHeight uint32) error
	IsActive() bool
	IsInitialized() bool
	GetUnknownMPTNodesBatch(limit int) []util.Uint256
	NeedHeaders() bool
	NeedMPTNodes() bool
	Traverse(root util.Uint256, process func(node mpt.Node, nodeBytes []byte) bool) error
}

StateSync represents state sync module.

type TCPPeer

type TCPPeer struct {
	// contains filtered or unexported fields
}

TCPPeer represents a connected remote node in the network over TCP.

func NewTCPPeer

func NewTCPPeer(conn net.Conn, s *Server) *TCPPeer

NewTCPPeer returns a TCPPeer structure based on the given connection.

func (*TCPPeer) AddGetAddrSent added in v0.92.0

func (p *TCPPeer) AddGetAddrSent()

AddGetAddrSent increments internal outstanding getaddr requests counter. Then, the peer can only send one addr reply per getaddr request.

func (*TCPPeer) CanProcessAddr added in v0.92.0

func (p *TCPPeer) CanProcessAddr() bool

CanProcessAddr decrements internal outstanding getaddr requests counter and answers whether the addr command from the peer can be safely processed.

func (*TCPPeer) Disconnect

func (p *TCPPeer) Disconnect(err error)

Disconnect will fill the peer's done channel with the given error.

func (*TCPPeer) EnqueueHPPacket

func (p *TCPPeer) EnqueueHPPacket(block bool, msg []byte) error

EnqueueHPPacket implements the Peer interface. It the peer is not yet handshaked it's a noop.

func (*TCPPeer) EnqueueMessage

func (p *TCPPeer) EnqueueMessage(msg *Message) error

EnqueueMessage is a temporary wrapper that sends a message via EnqueuePacket if there is no error in serializing it.

func (*TCPPeer) EnqueueP2PMessage

func (p *TCPPeer) EnqueueP2PMessage(msg *Message) error

EnqueueP2PMessage implements the Peer interface.

func (*TCPPeer) EnqueueP2PPacket

func (p *TCPPeer) EnqueueP2PPacket(msg []byte) error

EnqueueP2PPacket implements the Peer interface.

func (*TCPPeer) EnqueuePacket

func (p *TCPPeer) EnqueuePacket(block bool, msg []byte) error

EnqueuePacket implements the Peer interface.

func (*TCPPeer) HandlePing added in v0.91.0

func (p *TCPPeer) HandlePing(ping *payload.Ping) error

HandlePing handles a ping message received from the peer.

func (*TCPPeer) HandlePong

func (p *TCPPeer) HandlePong(pong *payload.Ping) error

HandlePong handles a pong message received from the peer and does an appropriate accounting of outstanding pings and timeouts.

func (*TCPPeer) HandleVersion

func (p *TCPPeer) HandleVersion(version *payload.Version) error

HandleVersion checks for the handshake state and version message contents.

func (*TCPPeer) HandleVersionAck

func (p *TCPPeer) HandleVersionAck() error

HandleVersionAck checks handshake sequence correctness when VerAck message is received.

func (*TCPPeer) Handshaked

func (p *TCPPeer) Handshaked() bool

Handshaked returns status of the handshake, whether it's completed or not.

func (*TCPPeer) IsFullNode added in v0.90.0

func (p *TCPPeer) IsFullNode() bool

IsFullNode returns whether the node has full capability or TCP/WS only.

func (*TCPPeer) LastBlockIndex

func (p *TCPPeer) LastBlockIndex() uint32

LastBlockIndex returns the last block index.

func (*TCPPeer) PeerAddr

func (p *TCPPeer) PeerAddr() net.Addr

PeerAddr implements the Peer interface.

func (*TCPPeer) RemoteAddr

func (p *TCPPeer) RemoteAddr() net.Addr

RemoteAddr implements the Peer interface.

func (*TCPPeer) SendPing

func (p *TCPPeer) SendPing(msg *Message) error

SendPing sends a ping message to the peer and does an appropriate accounting of outstanding pings and timeouts.

func (*TCPPeer) SendVersion

func (p *TCPPeer) SendVersion() error

SendVersion checks for the handshake state and sends a message to the peer.

func (*TCPPeer) SendVersionAck

func (p *TCPPeer) SendVersionAck(msg *Message) error

SendVersionAck checks for the handshake state and sends a message to the peer.

func (*TCPPeer) StartProtocol

func (p *TCPPeer) StartProtocol()

StartProtocol starts a long running background loop that interacts every ProtoTickInterval with the peer. It's only good to run after the handshake.

func (*TCPPeer) Version

func (p *TCPPeer) Version() *payload.Version

Version implements the Peer interface.

type TCPTransport

type TCPTransport struct {
	// contains filtered or unexported fields
}

TCPTransport allows network communication over TCP.

func NewTCPTransport

func NewTCPTransport(s *Server, bindAddr string, log *zap.Logger) *TCPTransport

NewTCPTransport returns a new TCPTransport that will listen for new incoming peer connections.

func (*TCPTransport) Accept

func (t *TCPTransport) Accept()

Accept implements the Transporter interface.

func (*TCPTransport) Address added in v0.90.0

func (t *TCPTransport) Address() string

Address implements the Transporter interface.

func (*TCPTransport) Close

func (t *TCPTransport) Close()

Close implements the Transporter interface.

func (*TCPTransport) Dial

func (t *TCPTransport) Dial(addr string, timeout time.Duration) error

Dial implements the Transporter interface.

func (*TCPTransport) Proto

func (t *TCPTransport) Proto() string

Proto implements the Transporter interface.

type Transporter

type Transporter interface {
	Dial(addr string, timeout time.Duration) error
	Accept()
	Proto() string
	Address() string
	Close()
}

Transporter is an interface that allows us to abstract any form of communication between the server and its peers.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL