activation

package
v0.1.17 Latest Latest
Warning

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

Go to latest
Published: Nov 19, 2020 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package activation is responsible for creating activation transactions and running the mining flow, coordinating PoST building, sending proofs to PoET and building NIPoST structs.

Index

Constants

View Source
const (
	// InitIdle status means that an init file does not exist and is not prepared
	InitIdle = 1 + iota
	// InitInProgress status means that an init file preparation is now in progress
	InitInProgress
	// InitDone status indicates there is a prepared init file
	InitDone
)
View Source
const AtxProtocol = "AtxGossip"

AtxProtocol is the protocol id for broadcasting atxs over gossip

View Source
const PoetProofProtocol = "PoetProof"

PoetProofProtocol is the name of the PoetProof gossip protocol.

Variables

This section is empty.

Functions

func DefaultConfig

func DefaultConfig() config.Config

DefaultConfig defines the default configuration options for PoST

func ExtractPublicKey

func ExtractPublicKey(signedAtx *types.ActivationTx) (*signing.PublicKey, error)

ExtractPublicKey extracts public key from message and verifies public key exists in idStore, this is how we validate ATX signature. If this is the first ATX it is considered valid anyways and ATX syntactic validation will determine ATX validity

func NewNIPSTWithChallenge

func NewNIPSTWithChallenge(challenge *types.Hash32, poetRef []byte) *types.NIPST

NewNIPSTWithChallenge is a convenience method FOR TESTS ONLY. TODO: move this out of production code.

func SignAtx

func SignAtx(signer signer, atx *types.ActivationTx) error

SignAtx signs the atx atx with specified signer and assigns the signature into atx.Sig this function returns an error if atx could not be converted to bytes

Types

type ActivesetCache

type ActivesetCache struct {
	*lru.Cache
}

ActivesetCache holds an lru cache of the active set size for a view hash.

func NewActivesetCache

func NewActivesetCache(size int) ActivesetCache

NewActivesetCache creates a cache for Active set size

func (*ActivesetCache) Add

func (bc *ActivesetCache) Add(view types.Hash12, setSize uint32)

Add adds a view hash and a set size that was calculated for this view

func (ActivesetCache) Get

func (bc ActivesetCache) Get(view types.Hash12) (uint32, bool)

Get returns the stored active set size for the provided view hash

type AtxCache

type AtxCache struct {
	*lru.Cache
}

AtxCache holds an lru cache of ActivationTxHeader structs of recent atx used to calculate active set size ideally this cache will hold the atxs created in latest epoch, on which most of active set size calculation will be performed

func NewAtxCache

func NewAtxCache(size int) AtxCache

NewAtxCache creates a new cache for activation transaction headers

func (*AtxCache) Add

func (bc *AtxCache) Add(id types.ATXID, atxHeader *types.ActivationTxHeader)

Add adds an activationTxHeader to cache

func (AtxCache) Get

Get gets the corresponding Atx header to the given id, it also returns a boolean to indicate whether the item was found in cache

type AtxMemDB added in v0.1.15

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

AtxMemDB is a memory store that holds all received ATXs from gossip network by their ids

func NewAtxMemPool added in v0.1.15

func NewAtxMemPool() *AtxMemDB

NewAtxMemPool creates a struct holding atxs by id

func (*AtxMemDB) GetAllItems added in v0.1.15

func (mem *AtxMemDB) GetAllItems() []*types.ActivationTx

GetAllItems creates and returns a list of all items found in cache

func (*AtxMemDB) GetEpochAtxs added in v0.1.15

func (mem *AtxMemDB) GetEpochAtxs(epochID types.EpochID) (atxs []types.ATXID)

GetEpochAtxs returns mock atx list

func (*AtxMemDB) GetFullAtx added in v0.1.15

func (mem *AtxMemDB) GetFullAtx(id types.ATXID) (*types.ActivationTx, error)

GetFullAtx retrieves the atx by the provided id id, it returns a reference to the found atx struct or an error if not

func (*AtxMemDB) Invalidate added in v0.1.15

func (mem *AtxMemDB) Invalidate(id types.ATXID)

Invalidate removes the provided atx by its id. it does not return error if id is not found

func (*AtxMemDB) ProcessAtx added in v0.1.15

func (mem *AtxMemDB) ProcessAtx(atx *types.ActivationTx) error

ProcessAtx puts an atx into mem pool, this is just for testing

func (*AtxMemDB) Put added in v0.1.15

func (mem *AtxMemDB) Put(atx *types.ActivationTx)

Put insets an atx into the mempool

type Builder

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

Builder struct is the struct that orchestrates the creation of activation transactions it is responsible for initializing post, receiving poet proof and orchestrating nipst. after which it will calculate active set size and providing relevant view as proof

func NewBuilder

func NewBuilder(nodeID types.NodeID, coinbaseAccount types.Address, signer signer, db atxDBProvider, net broadcaster, mesh meshProvider, layersPerEpoch uint16, nipstBuilder nipstBuilder, postProver PostProverClient, layerClock layerClock, syncer syncer, store bytesStore, log log.Log) *Builder

NewBuilder returns an atx builder that will start a routine that will attempt to create an atx upon each new layer.

func (*Builder) GetPositioningAtx

func (b *Builder) GetPositioningAtx() (*types.ActivationTxHeader, error)

GetPositioningAtx return the latest atx to be used as a positioning atx

func (*Builder) GetPrevAtx

func (b *Builder) GetPrevAtx(node types.NodeID) (*types.ActivationTxHeader, error)

GetPrevAtx gets the last atx header of specified node Id, it returns error if no previous atx found or if no AtxHeader struct in db

func (*Builder) GetSmesherID added in v0.1.15

func (b *Builder) GetSmesherID() types.NodeID

GetSmesherID returns the ID of the smesher that created this activation

func (*Builder) MiningStats

func (b *Builder) MiningStats() (int, uint64, string, string)

MiningStats returns state of post init, coinbase reward account and data directory path for post commitment

func (*Builder) PublishActivationTx

func (b *Builder) PublishActivationTx() error

PublishActivationTx attempts to publish an atx, it returns an error if an atx cannot be created.

func (*Builder) SetCoinbaseAccount

func (b *Builder) SetCoinbaseAccount(rewardAddress types.Address)

SetCoinbaseAccount sets the address rewardAddress to be the coinbase account written into the activation transaction the rewards for blocks made by this miner will go to this address

func (*Builder) SignAtx

func (b *Builder) SignAtx(atx *types.ActivationTx) error

SignAtx signs the atx and assigns the signature into atx.Sig this function returns an error if atx could not be converted to bytes

func (*Builder) Start

func (b *Builder) Start()

Start is the main entry point of the atx builder. it runs the main loop of the builder and shouldn't be called more than once

func (*Builder) StartPost

func (b *Builder) StartPost(rewardAddress types.Address, dataDir string, space uint64) error

StartPost initiates post commitment generation process. It returns an error if a process is already in progress or if a post has been already initialized

func (*Builder) Stop

func (b *Builder) Stop()

Stop stops the atx builder.

type DB added in v0.1.11

type DB struct {
	sync.RWMutex

	LayersPerEpoch uint16
	// contains filtered or unexported fields
}

DB hold the atxs received from all nodes and their validity status it also stores identifications for all nodes e.g the coupling between ed id and bls id

func NewDB added in v0.1.11

func NewDB(dbStore database.Database, idStore idStore, meshDb *mesh.DB, layersPerEpoch uint16, nipstValidator nipstValidator, log log.Log) *DB

NewDB creates a new struct of type DB, this struct will hold the atxs received from all nodes and their validity

func (*DB) AwaitAtx added in v0.1.11

func (db *DB) AwaitAtx(id types.ATXID) chan struct{}

AwaitAtx returns a channel that will receive notification when the specified atx with id id is received via gossip

func (*DB) CalcActiveSetFromView added in v0.1.11

func (db *DB) CalcActiveSetFromView(view []types.BlockID, pubEpoch types.EpochID) (uint32, error)

CalcActiveSetFromView traverses the view found in a - the activation tx and counts number of active ids published in the epoch prior to the epoch that a was published at, this number is the number of active ids in the next epoch the function returns error if the view is not found

func (*DB) CalcActiveSetSize added in v0.1.11

func (db *DB) CalcActiveSetSize(epoch types.EpochID, blocks map[types.BlockID]struct{}) (map[string]struct{}, error)

CalcActiveSetSize - returns the active set size that matches the view of the contextually valid blocks in the provided layer

func (*DB) ContextuallyValidateAtx added in v0.1.11

func (db *DB) ContextuallyValidateAtx(atx *types.ActivationTxHeader) error

ContextuallyValidateAtx ensures that the previous ATX referenced is the last known ATX for the referenced miner ID. If a previous ATX is not referenced, it validates that indeed there's no previous known ATX for that miner ID.

func (*DB) FetchAtxReferences added in v0.1.16

func (db *DB) FetchAtxReferences(atx *types.ActivationTx, f service.Fetcher) error

FetchAtxReferences fetches positioning and prev atxs from peers if they are not found in db

func (*DB) GetAtxHeader added in v0.1.11

func (db *DB) GetAtxHeader(id types.ATXID) (*types.ActivationTxHeader, error)

GetAtxHeader returns the ATX header by the given ID. This function is thread safe and will return an error if the ID is not found in the ATX DB.

func (*DB) GetEpochAtxs added in v0.1.15

func (db *DB) GetEpochAtxs(epochID types.EpochID) (atxs []types.ATXID)

GetEpochAtxs returns all valid ATXs received in the epoch epochID

func (*DB) GetFullAtx added in v0.1.11

func (db *DB) GetFullAtx(id types.ATXID) (*types.ActivationTx, error)

GetFullAtx returns the full atx struct of the given atxId id, it returns an error if the full atx cannot be found in all databases

func (*DB) GetNodeAtxIDForEpoch added in v0.1.11

func (db *DB) GetNodeAtxIDForEpoch(nodeID types.NodeID, targetEpoch types.EpochID) (types.ATXID, error)

GetNodeAtxIDForEpoch returns an atx published by the provided nodeID for the specified targetEpoch. meaning the atx that the requested nodeID has published. it returns an error if no atx was found for provided nodeID

func (*DB) GetNodeLastAtxID added in v0.1.11

func (db *DB) GetNodeLastAtxID(nodeID types.NodeID) (types.ATXID, error)

GetNodeLastAtxID returns the last atx id that was received for node nodeID

func (*DB) GetPosAtxID added in v0.1.11

func (db *DB) GetPosAtxID() (types.ATXID, error)

GetPosAtxID returns the best (highest layer id), currently known to this node, pos atx id

func (*DB) HandleAtxData added in v0.1.16

func (db *DB) HandleAtxData(data []byte, syncer service.Fetcher) error

HandleAtxData handles atxs received either by gossip or sync

func (*DB) HandleGossipAtx added in v0.1.15

func (db *DB) HandleGossipAtx(data service.GossipMessage, syncer service.Fetcher)

HandleGossipAtx handles the atx gossip data channel

func (*DB) ProcessAtx added in v0.1.11

func (db *DB) ProcessAtx(atx *types.ActivationTx) error

ProcessAtx validates the active set size declared in the atx, and contextually validates the atx according to atx validation rules it then stores the atx with flag set to validity of the atx.

ATXs received as input must be already syntactically valid. Only contextual validation is performed.

func (*DB) ProcessAtxs added in v0.1.11

func (db *DB) ProcessAtxs(atxs []*types.ActivationTx) error

ProcessAtxs processes the list of given atxs using ProcessAtx method

func (*DB) StoreAtx added in v0.1.11

func (db *DB) StoreAtx(ech types.EpochID, atx *types.ActivationTx) error

StoreAtx stores an atx for epoch ech, it stores atx for the current epoch and adds the atx for the nodeID that created it in a sorted manner by the sequence id. This function does not validate the atx and assumes all data is correct and that all associated atx exist in the db. Will return error if writing to db failed.

func (*DB) SyntacticallyValidateAtx added in v0.1.11

func (db *DB) SyntacticallyValidateAtx(atx *types.ActivationTx) error

SyntacticallyValidateAtx ensures the following conditions apply, otherwise it returns an error.

  • If the sequence number is non-zero: PrevATX points to a syntactically valid ATX whose sequence number is one less than the current ATX's sequence number.
  • If the sequence number is zero: PrevATX is empty.
  • Positioning ATX points to a syntactically valid ATX.
  • NIPST challenge is a hash of the serialization of the following fields: NodeID, SequenceNumber, PrevATXID, LayerID, StartTick, PositioningATX.
  • The NIPST is valid.
  • ATX LayerID is NipstLayerTime or less after the PositioningATX LayerID.
  • The ATX view of the previous epoch contains ActiveSetSize activations.

func (*DB) UnsubscribeAtx added in v0.1.11

func (db *DB) UnsubscribeAtx(id types.ATXID)

UnsubscribeAtx un subscribes the waiting for a specific atx with atx id id to arrive via gossip.

func (*DB) ValidateSignedAtx added in v0.1.11

func (db *DB) ValidateSignedAtx(pubKey signing.PublicKey, signedAtx *types.ActivationTx) error

ValidateSignedAtx extracts public key from message and verifies public key exists in idStore, this is how we validate ATX signature. If this is the first ATX it is considered valid anyways and ATX syntactic validation will determine ATX validity

type ErrAtxNotFound

type ErrAtxNotFound error

ErrAtxNotFound is a specific error returned when no atx was found in DB

type GetInfoResponse

type GetInfoResponse struct {
	OpenRoundID        string
	ExecutingRoundsIDs []string
	ServicePubKey      []byte
}

GetInfoResponse is the response object for the get-info endpoint

type HTTPPoetClient

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

HTTPPoetClient implements PoetProvingServiceClient interface.

func NewHTTPPoetClient

func NewHTTPPoetClient(ctx context.Context, target string) *HTTPPoetClient

NewHTTPPoetClient returns new instance of HTTPPoetClient for the specified target.

func (*HTTPPoetClient) PoetServiceID added in v0.1.11

func (c *HTTPPoetClient) PoetServiceID() ([]byte, error)

PoetServiceID returns the public key of the PoET proving service.

func (*HTTPPoetClient) Start

func (c *HTTPPoetClient) Start(gatewayAddresses []string) error

Start is an administrative endpoint of the proving service that tells it to start. This is mostly done in tests, since it requires administrative permissions to the proving service.

func (*HTTPPoetClient) Submit

func (c *HTTPPoetClient) Submit(challenge types.Hash32) (*types.PoetRound, error)

Submit registers a challenge in the proving service current open round.

type HTTPPoetHarness

type HTTPPoetHarness struct {
	*HTTPPoetClient
	Teardown func(cleanup bool) error
	// contains filtered or unexported fields
}

HTTPPoetHarness utilizes a local self-contained poet server instance targeted by an HTTP client, in order to exercise functionality.

func NewHTTPPoetHarness

func NewHTTPPoetHarness(disableBroadcast bool) (*HTTPPoetHarness, error)

NewHTTPPoetHarness returns a new instance of HTTPPoetHarness.

type IdentityStore

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

IdentityStore stores couples of identities and used to retrieve bls identity by provided ed25519 identity

func NewIdentityStore

func NewIdentityStore(db database.Database) *IdentityStore

NewIdentityStore creates a new identity store

func (*IdentityStore) GetIdentity

func (s *IdentityStore) GetIdentity(id string) (types.NodeID, error)

GetIdentity gets the identity by the provided ed25519 string id, it returns a NodeID struct or an error if id was not found

func (*IdentityStore) StoreNodeIdentity

func (s *IdentityStore) StoreNodeIdentity(id types.NodeID) error

StoreNodeIdentity stores a NodeID type, which consists of 2 identities: BLS and ed25519

type NIPSTBuilder

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

NIPSTBuilder holds the required state and dependencies to create Non-Interactive Proofs of Space-Time (NIPST).

func NewNIPSTBuilder

func NewNIPSTBuilder(
	minerID []byte,
	postProver PostProverClient,
	poetProver PoetProvingServiceClient,
	poetDB poetDbAPI,
	store bytesStore,
	log log.Log,
) *NIPSTBuilder

NewNIPSTBuilder returns a NIPSTBuilder.

func (*NIPSTBuilder) BuildNIPST

func (nb *NIPSTBuilder) BuildNIPST(challenge *types.Hash32, atxExpired, stop chan struct{}) (*types.NIPST, error)

BuildNIPST uses the given challenge to build a NIPST. "atxExpired" and "stop" are channels for early termination of the building process. The process can take considerable time, because it includes waiting for the poet service to publish a proof - a process that takes about an epoch.

type PoetDb

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

PoetDb is a database for PoET proofs.

func NewPoetDb

func NewPoetDb(store database.Database, log log.Log) *PoetDb

NewPoetDb returns a new PoET DB.

func (*PoetDb) GetMembershipMap

func (db *PoetDb) GetMembershipMap(proofRef []byte) (map[types.Hash32]bool, error)

GetMembershipMap returns the map of memberships in the requested PoET proof.

func (*PoetDb) GetProofMessage

func (db *PoetDb) GetProofMessage(proofRef []byte) ([]byte, error)

GetProofMessage returns the originally received PoET proof message.

func (*PoetDb) HasProof

func (db *PoetDb) HasProof(proofRef []byte) bool

HasProof returns true if the database contains a proof with the given reference, or false otherwise.

func (*PoetDb) SubscribeToProofRef

func (db *PoetDb) SubscribeToProofRef(poetID []byte, roundID string) chan []byte

SubscribeToProofRef returns a channel that PoET proof ref for the requested PoET ID and round ID will be sent. If the proof is already available it will be sent immediately, otherwise it will be sent when available.

func (*PoetDb) UnsubscribeFromProofRef

func (db *PoetDb) UnsubscribeFromProofRef(poetID []byte, roundID string)

UnsubscribeFromProofRef removes all subscriptions from a given poetID and roundID. This method should be used with caution since any subscribers still waiting will now hang forever. TODO: only cancel specific subscription.

func (*PoetDb) Validate

func (db *PoetDb) Validate(proof types.PoetProof, poetID []byte, roundID string, signature []byte) error

Validate validates a new PoET proof.

func (*PoetDb) ValidateAndStore

func (db *PoetDb) ValidateAndStore(proofMessage *types.PoetProofMessage) error

ValidateAndStore validates and stores a new PoET proof.

func (*PoetDb) ValidateAndStoreMsg added in v0.1.16

func (db *PoetDb) ValidateAndStoreMsg(data []byte) error

ValidateAndStoreMsg validates and stores a new PoET proof.

type PoetListener

type PoetListener struct {
	Log log.Log
	// contains filtered or unexported fields
}

PoetListener handles PoET gossip messages.

func NewPoetListener

func NewPoetListener(net service.Service, poetDb poetValidatorPersistor, logger log.Log) *PoetListener

NewPoetListener returns a new PoetListener.

func (*PoetListener) Close

func (l *PoetListener) Close()

Close performs graceful shutdown of the PoET listener.

func (*PoetListener) Start

func (l *PoetListener) Start()

Start starts listening to PoET gossip messages.

type PoetProvingServiceClient

type PoetProvingServiceClient interface {
	// Submit registers a challenge in the proving service current open round.
	Submit(challenge types.Hash32) (*types.PoetRound, error)

	// PoetServiceID returns the public key of the PoET proving service.
	PoetServiceID() ([]byte, error)
}

PoetProvingServiceClient provides a gateway to a trust-less public proving service, which may serve many PoET proving clients, and thus enormously reduce the cost-per-proof for PoET since each additional proof adds only a small number of hash evaluations to the total cost.

type PostClient

type PostClient struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

PostClient consolidates Proof of Space-Time functionality like initializing space and executing proofs.

func NewPostClient

func NewPostClient(cfg *config.Config, minerID []byte) (*PostClient, error)

NewPostClient returns a new PostClient based on a configuration and minerID

func (*PostClient) Cfg

func (c *PostClient) Cfg() *config.Config

Cfg returns the the client latest config.

func (*PostClient) Execute

func (c *PostClient) Execute(challenge []byte) (*types.PostProof, error)

Execute is the phase in which the prover received a challenge, and proves that his data is still stored (or was recomputed). This phase can be repeated arbitrarily many times without repeating initialization; thus despite the initialization essentially serving as a proof-of-work, the amortized computational complexity can be made arbitrarily small.

func (*PostClient) Initialize

func (c *PostClient) Initialize() (commitment *types.PostProof, err error)

Initialize is the process in which the prover commits to store some data, by having its storage filled with pseudo-random data with respect to a specific id. This data is the result of a computationally-expensive operation.

func (*PostClient) IsInitialized

func (c *PostClient) IsInitialized() (initComplete bool, remainingBytes uint64, err error)

IsInitialized indicates whether the initialization phase has been completed. If it's not complete the remaining bytes are also returned.

func (*PostClient) Reset

func (c *PostClient) Reset() error

Reset removes the initialization phase files.

func (*PostClient) SetLogger

func (c *PostClient) SetLogger(logger shared.Logger)

SetLogger sets a logger for the client.

func (*PostClient) SetParams

func (c *PostClient) SetParams(dataDir string, space uint64) error

SetParams updates the datadir and space params in the client config, to be used in the initialization and the execution phases. It overrides the config which the client was instantiated with.

func (*PostClient) VerifyInitAllowed

func (c *PostClient) VerifyInitAllowed() error

VerifyInitAllowed indicates whether the preconditions for starting the initialization phase are met.

type PostProverClient

type PostProverClient interface {
	// Initialize is the process in which the prover commits to store some data, by having its storage filled with
	// pseudo-random data with respect to a specific id. This data is the result of a computationally-expensive
	// operation.
	Initialize() (commitment *types.PostProof, err error)

	// Execute is the phase in which the prover received a challenge, and proves that his data is still stored (or was
	// recomputed). This phase can be repeated arbitrarily many times without repeating initialization; thus despite the
	// initialization essentially serving as a proof-of-work, the amortized computational complexity can be made
	// arbitrarily small.
	Execute(challenge []byte) (proof *types.PostProof, err error)

	// Reset removes the initialization phase files.
	Reset() error

	// IsInitialized indicates whether the initialization phase has been completed. If it's not complete the remaining
	// bytes are also returned.
	IsInitialized() (initComplete bool, remainingBytes uint64, err error)

	// VerifyInitAllowed indicates whether the preconditions for starting
	// the initialization phase are met.
	VerifyInitAllowed() error

	// SetParams updates the datadir and space params in the client config, to be used in the initialization and the
	// execution phases. It overrides the config which the client was instantiated with.
	SetParams(datadir string, space uint64) error

	// SetLogger sets a logger for the client.
	SetLogger(logger shared.Logger)

	// Cfg returns the the client latest config.
	Cfg() *config.Config
}

PostProverClient provides proving functionality for PoST.

type StartRequest

type StartRequest struct {
	GatewayAddresses       []string `json:"gatewayAddresses,omitempty"`
	DisableBroadcast       bool     `json:"disableBroadcast,omitempty"`
	ConnAcksThreshold      int      `json:"connAcksThreshold,omitempty"`
	BroadcastAcksThreshold int      `json:"broadcastAcksThreshold,omitempty"`
}

StartRequest is the request object for the start endpoint

type StopRequestedError

type StopRequestedError struct{}

StopRequestedError is a specific type of error the indicated a user has stopped mining

func (StopRequestedError) Error

func (s StopRequestedError) Error() string

type SubmitRequest

type SubmitRequest struct {
	Challenge []byte `json:"challenge,omitempty"`
}

SubmitRequest is the request object for the submit endpoint

type SubmitResponse

type SubmitResponse struct {
	RoundID string
}

SubmitResponse is the response object for the submit endpoint

type Validator

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

Validator contains the dependencies required to validate NIPSTs

func NewValidator

func NewValidator(postCfg *config.Config, poetDb poetDbAPI) *Validator

NewValidator returns a new NIPST validator

func (*Validator) Validate

func (v *Validator) Validate(minerID signing.PublicKey, nipst *types.NIPST, expectedChallenge types.Hash32) error

Validate validates a NIPST, given a miner id and expected challenge. It returns an error if an issue is found or nil if the NIPST is valid.

func (*Validator) VerifyPost

func (v *Validator) VerifyPost(minerID signing.PublicKey, proof *types.PostProof, space uint64) error

VerifyPost validates a Proof of Space-Time (PoST). It returns nil if validation passed or an error indicating why validation failed.

Jump to

Keyboard shortcuts

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