testing

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2022 License: Apache-2.0 Imports: 58 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// client types
	Tendermint  = exported.Tendermint
	SoloMachine = exported.Solomachine
	Fabric      = fabrictypes.Fabric

	// Default params constants used to create a TM client
	TrustingPeriod     time.Duration = time.Hour * 24 * 7 * 2
	UnbondingPeriod    time.Duration = time.Hour * 24 * 7 * 3
	MaxClockDrift      time.Duration = time.Second * 10
	DefaultDelayPeriod uint64        = 0

	ChannelTransferVersion = ibctransfertypes.Version

	ConnectionIDPrefix = "conn"
	ChannelIDPrefix    = "chan"

	TransferPort = ibctransfertypes.ModuleName
	MockPort     = mock.ModuleName
)

Variables

View Source
var (
	DefaultOpenInitVersion *connectiontypes.Version
	ConnectionVersion      = connectiontypes.ExportedVersionsToProto(connectiontypes.GetCompatibleVersions())[0]

	// Default params variables used to create a TM client
	DefaultTrustLevel ibctmtypes.Fraction = ibctmtypes.DefaultTrustLevel
	TestHash                              = tmhash.Sum([]byte("TESTING HASH"))
	TestCoin                              = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))

	UpgradePath = []string{"upgrade", "upgradedIBCState"}
)
View Source
var (
	ChainIDPrefix = "testchain"
)

Functions

func GetChainID

func GetChainID(index int) string

GetChainID returns the chainID used for the provided index.

Types

type Account

type Account struct {
	authtypes.AccountI
	// contains filtered or unexported fields
}

func NewAccount

func NewAccount(base authtypes.AccountI, sid *msp.SerializedIdentity) *Account

func (Account) GetAddress

func (acc Account) GetAddress() sdk.AccAddress

type Coordinator

type Coordinator struct {
	Chains map[string]TestChainI
	// contains filtered or unexported fields
}

func NewCoordinator

func NewCoordinator(t *testing.T, n int, mspID string, txSignMode TxSignMode) *Coordinator

func (*Coordinator) AcknowledgePacket

func (coord *Coordinator) AcknowledgePacket(
	source, counterparty TestChainI,
	counterpartyClient string,
	packet channeltypes.Packet, ack []byte,
) error

AcknowledgePacket acknowledges on the source chain the packet received on the counterparty chain and updates the client on the counterparty representing the source chain. TODO: add a query for the acknowledgement by events - https://github.com/cosmos/cosmos-sdk/issues/6509

func (*Coordinator) ChanCloseInit

func (coord *Coordinator) ChanCloseInit(
	source, counterparty TestChainI,
	channel TestChannel,
) error

ChanCloseInit closes a channel on the source chain resulting in the channels state being set to CLOSED.

NOTE: does not work with ibc-transfer module

func (*Coordinator) ChanOpenAck

func (coord *Coordinator) ChanOpenAck(
	source, counterparty TestChainI,
	sourceChannel, counterpartyChannel TestChannel,
) error

ChanOpenAck initializes a channel on the source chain with the state OPEN using the OpenAck handshake call.

func (*Coordinator) ChanOpenConfirm

func (coord *Coordinator) ChanOpenConfirm(
	source, counterparty TestChainI,
	sourceChannel, counterpartyChannel TestChannel,
) error

ChanOpenConfirm initializes a channel on the source chain with the state OPEN using the OpenConfirm handshake call.

func (*Coordinator) ChanOpenInit

func (coord *Coordinator) ChanOpenInit(
	source, counterparty TestChainI,
	connection, counterpartyConnection *TestConnection,
	sourcePortID, counterpartyPortID string,
	order channeltypes.Order,
) (TestChannel, TestChannel, error)

ChanOpenInit initializes a channel on the source chain with the state INIT using the OpenInit handshake call.

NOTE: The counterparty testing channel will be created even if it is not created in the application state.

func (*Coordinator) ChanOpenTry

func (coord *Coordinator) ChanOpenTry(
	source, counterparty TestChainI,
	sourceChannel, counterpartyChannel TestChannel,
	connection *TestConnection,
	order channeltypes.Order,
) error

ChanOpenTry initializes a channel on the source chain with the state TRYOPEN using the OpenTry handshake call.

func (*Coordinator) CommitBlock

func (coord *Coordinator) CommitBlock(chains ...TestChainI)

CommitBlock commits a block on the provided indexes and then increments the global time.

CONTRACT: the passed in list of indexes must not contain duplicates

func (*Coordinator) ConnOpenAck

func (coord *Coordinator) ConnOpenAck(
	source, counterparty TestChainI,
	sourceConnection, counterpartyConnection *TestConnection,
) error

ConnOpenAck initializes a connection on the source chain with the state OPEN using the OpenAck handshake call.

func (*Coordinator) ConnOpenConfirm

func (coord *Coordinator) ConnOpenConfirm(
	source, counterparty TestChainI,
	sourceConnection, counterpartyConnection *TestConnection,
) error

ConnOpenConfirm initializes a connection on the source chain with the state OPEN using the OpenConfirm handshake call.

func (*Coordinator) ConnOpenInit

func (coord *Coordinator) ConnOpenInit(
	source, counterparty TestChainI,
	clientID, counterpartyClientID string, nextChannelVersion string,
) (*TestConnection, *TestConnection, error)

ConnOpenInit initializes a connection on the source chain with the state INIT using the OpenInit handshake call.

NOTE: The counterparty testing connection will be created even if it is not created in the application state.

func (*Coordinator) ConnOpenTry

func (coord *Coordinator) ConnOpenTry(
	source, counterparty TestChainI,
	sourceConnection, counterpartyConnection *TestConnection,
) error

ConnOpenTry initializes a connection on the source chain with the state TRYOPEN using the OpenTry handshake call.

func (*Coordinator) CreateChannel

func (coord *Coordinator) CreateChannel(
	chainA, chainB TestChainI,
	connA, connB *TestConnection,
	sourcePortID, counterpartyPortID string,
	order channeltypes.Order,
) (TestChannel, TestChannel)

CreateChannel constructs and executes channel handshake messages in order to create OPEN channels on chainA and chainB. The function expects the channels to be successfully opened otherwise testing will fail.

func (*Coordinator) CreateClient

func (coord *Coordinator) CreateClient(
	source, counterparty TestChainI,
	clientType string,
) (clientID string, err error)

CreateClient creates a counterparty client on the source chain and returns the clientID.

func (*Coordinator) CreateConnection

func (coord *Coordinator) CreateConnection(
	chainA, chainB TestChainI,
	clientA, clientB string,
	nextChannelVersion string,
) (*TestConnection, *TestConnection)

CreateConnection constructs and executes connection handshake messages in order to create OPEN channels on chainA and chainB. The connection information of for chainA and chainB are returned within a TestConnection struct. The function expects the connections to be successfully opened otherwise testing will fail.

func (*Coordinator) CreateMockChannels

func (coord *Coordinator) CreateMockChannels(
	chainA, chainB TestChainI,
	connA, connB *TestConnection,
	order channeltypes.Order,
) (TestChannel, TestChannel)

CreateMockChannels constructs and executes channel handshake messages to create OPEN channels that use a mock application module that returns nil on all callbacks. This function is expects the channels to be successfully opened otherwise testing will fail.

func (*Coordinator) CreateTransferChannels

func (coord *Coordinator) CreateTransferChannels(
	chainA, chainB TestChainI,
	connA, connB *TestConnection,
	order channeltypes.Order,
) (TestChannel, TestChannel)

CreateTransferChannels constructs and executes channel handshake messages to create OPEN ibc-transfer channels on chainA and chainB. The function expects the channels to be successfully opened otherwise testing will fail.

func (*Coordinator) GetChain

func (coord *Coordinator) GetChain(chainID string) TestChainI

GetChain returns the TestChain using the given chainID and returns an error if it does not exist.

func (*Coordinator) IncrementTime

func (coord *Coordinator) IncrementTime()

IncrementTime iterates through all the TestChain's and increments their current header time by 5 seconds.

CONTRACT: this function must be called after every commit on any TestChain.

func (*Coordinator) RecvPacket

func (coord *Coordinator) RecvPacket(
	source, counterparty TestChainI,
	sourceClient string,
	packet channeltypes.Packet,
) error

RecvPacket receives a channel packet on the counterparty chain and updates the client on the source chain representing the counterparty.

func (*Coordinator) RelayPacket

func (coord *Coordinator) RelayPacket(
	source, counterparty TestChainI,
	sourceClient, counterpartyClient string,
	packet channeltypes.Packet, ack []byte,
) error

RelayPacket receives a channel packet on counterparty, queries the ack and acknowledges the packet on source. The clients are updated as needed.

func (*Coordinator) SendMsg

func (coord *Coordinator) SendMsg(source, counterparty TestChainI, counterpartyClientID string, msg sdk.Msg) error

SendMsg delivers a single provided message to the chain. The counterparty client is update with the new source consensus state.

func (*Coordinator) SendMsgs

func (coord *Coordinator) SendMsgs(source, counterparty TestChainI, counterpartyClientID string, msgs []sdk.Msg) error

SendMsgs delivers the provided messages to the chain. The counterparty client is updated with the new source consensus state.

func (*Coordinator) SetChannelClosed

func (coord *Coordinator) SetChannelClosed(
	source, counterparty TestChainI,
	testChannel TestChannel,
) error

SetChannelClosed sets a channel state to CLOSED.

func (*Coordinator) Setup

Setup constructs a TM client, connection, and channel on both chains provided. It will fail if any error occurs. The clientID's, TestConnections, and TestChannels are returned for both chains. The channels created are connected to the ibc-transfer application.

func (*Coordinator) SetupClientConnections

func (coord *Coordinator) SetupClientConnections(
	chainA, chainB TestChainI,
	clientType string,
) (string, string, *TestConnection, *TestConnection)

SetupClientConnections is a helper function to create clients and the appropriate connections on both the source and counterparty chain. It assumes the caller does not anticipate any errors.

func (*Coordinator) SetupClients

func (coord *Coordinator) SetupClients(
	chainA, chainB TestChainI,
	clientType string,
) (string, string)

SetupClients is a helper function to create clients on both chains. It assumes the caller does not anticipate any errors.

func (*Coordinator) UpdateClient

func (coord *Coordinator) UpdateClient(
	source, counterparty TestChainI,
	clientID string,
	clientType string,
) (err error)

UpdateClient updates a counterparty client on the source chain.

type TestChain

type TestChain struct {
	App  *simapp.IBCApp
	CC   *chaincode.IBCChaincode
	Stub *fabricmock.ChaincodeStub

	ChainID       string
	LastHeader    *ibctmtypes.Header // header for last block height committed
	CurrentHeader tmproto.Header     // header for current block height
	QueryServer   types.QueryServer
	TxConfig      client.TxConfig
	Codec         codec.BinaryCodec

	Vals    *tmtypes.ValidatorSet
	Signers []tmtypes.PrivValidator

	SenderAccount authtypes.AccountI

	// IBC specific helpers
	ClientIDs   []string          // ClientID's used on this chain
	Connections []*TestConnection // track connectionID's created for this chain

	NextChannelVersion string
	// contains filtered or unexported fields
}

TestChain is a testing struct that wraps a simapp with the last TM Header, the current ABCI header and the validators of the TestChain. It also contains a field called ChainID. This is the clientID that *other* chains use to refer to this TestChain. The SenderAccount is used for delivering transactions through the application state. NOTE: the actual application uses an empty chain-id for ease of testing.

func NewTestFabricChain

func NewTestFabricChain(t *testing.T, chainID string, mspID string, txSignMode TxSignMode) *TestChain

func (*TestChain) AddTestChannel

func (chain *TestChain) AddTestChannel(conn *TestConnection, portID string) TestChannel

AddTestChannel appends a new TestChannel which contains references to the port and channel ID used for channel creation and interaction. See 'NextTestChannel' for channel ID naming format.

func (*TestChain) AddTestConnection

func (chain *TestChain) AddTestConnection(clientID, counterpartyClientID, nextChannelVersion string) *TestConnection

AddTestConnection appends a new TestConnection which contains references to the connection id, client id and counterparty client id.

func (*TestChain) ChanCloseInit

func (chain *TestChain) ChanCloseInit(
	counterparty TestChainI,
	channel TestChannel,
) error

ChanCloseInit will construct and execute a MsgChannelCloseInit.

NOTE: does not work with ibc-transfer module

func (*TestChain) ChanOpenAck

func (chain *TestChain) ChanOpenAck(
	counterparty TestChainI,
	ch, counterpartyCh TestChannel,
) error

ChanOpenAck will construct and execute a MsgChannelOpenAck.

func (*TestChain) ChanOpenConfirm

func (chain *TestChain) ChanOpenConfirm(
	counterparty TestChainI,
	ch, counterpartyCh TestChannel,
) error

ChanOpenConfirm will construct and execute a MsgChannelOpenConfirm.

func (*TestChain) ChanOpenInit

func (chain *TestChain) ChanOpenInit(
	ch, counterparty TestChannel,
	order channeltypes.Order,
	connectionID string,
) error

ChanOpenInit will construct and execute a MsgChannelOpenInit.

func (*TestChain) ChanOpenTry

func (chain *TestChain) ChanOpenTry(
	counterparty TestChainI,
	ch, counterpartyCh TestChannel,
	order channeltypes.Order,
	connectionID string,
) error

ChanOpenTry will construct and execute a MsgChannelOpenTry.

func (*TestChain) ConnectionOpenAck

func (chain *TestChain) ConnectionOpenAck(
	counterparty TestChainI,
	connection, counterpartyConnection *TestConnection,
) error

ConnectionOpenAck will construct and execute a MsgConnectionOpenAck.

func (*TestChain) ConnectionOpenConfirm

func (chain *TestChain) ConnectionOpenConfirm(
	counterparty TestChainI,
	connection, counterpartyConnection *TestConnection,
) error

ConnectionOpenConfirm will construct and execute a MsgConnectionOpenConfirm.

func (*TestChain) ConnectionOpenInit

func (chain *TestChain) ConnectionOpenInit(
	counterparty TestChainI,
	connection, counterpartyConnection *TestConnection,
) error

ConnectionOpenInit will construct and execute a MsgConnectionOpenInit.

func (*TestChain) ConnectionOpenTry

func (chain *TestChain) ConnectionOpenTry(
	counterparty TestChainI,
	connection, counterpartyConnection *TestConnection,
) error

ConnectionOpenTry will construct and execute a MsgConnectionOpenTry.

func (*TestChain) ConstructMsgCreateClient

func (chain *TestChain) ConstructMsgCreateClient(counterparty TestChainI, clientID string, clientType string) *clienttypes.MsgCreateClient

ConstructMsgCreateClient constructs a message to create a new client state (tendermint or solomachine). NOTE: a solo machine client will be created with an empty diversifier.

func (*TestChain) ConstructNextTestConnection

func (chain *TestChain) ConstructNextTestConnection(clientID, counterpartyClientID, nextChannelVersion string) *TestConnection

ConstructNextTestConnection constructs the next test connection to be created given a clientID and counterparty clientID. The connection id format: <chainID>-conn<index>

func (*TestChain) ConstructUpdateClientHeader

func (*TestChain) ConstructUpdateClientHeader(counterparty TestChainI, clientID string) (exported.Header, error)

func (*TestChain) CreateChannelCapability

func (chain *TestChain) CreateChannelCapability(portID, channelID string)

CreateChannelCapability binds and claims a capability for the given portID and channelID if it does not already exist. This function will fail testing on any resulting error.

func (*TestChain) CreateClient

func (chain *TestChain) CreateClient(counterparty TestChainI, clientID string) error

func (*TestChain) CreatePortCapability

func (chain *TestChain) CreatePortCapability(portID string)

CreatePortCapability binds and claims a capability for the given portID if it does not already exist. This function will fail testing on any resulting error. NOTE: only creation of a capbility for a transfer or mock port is supported Other applications must bind to the port in InitGenesis or modify this code.

func (*TestChain) ExpireClient

func (chain *TestChain) ExpireClient(amount time.Duration)

ExpireClient fast forwards the chain's block time by the provided amount of time which will expire any clients with a trusting period less than or equal to this amount of time.

func (*TestChain) GetAcknowledgement

func (chain *TestChain) GetAcknowledgement(packet exported.PacketI) []byte

GetAcknowledgement retrieves an acknowledgement for the provided packet. If the acknowledgement does not exist then testing will fail.

func (TestChain) GetApp

func (chain TestChain) GetApp() interface{}

func (TestChain) GetChainID

func (chain TestChain) GetChainID() string

GetChainID implements TestChainI.GetChainID

func (*TestChain) GetChannel

func (chain *TestChain) GetChannel(testChannel TestChannel) channeltypes.Channel

GetChannel retrieves an IBC Channel for the provided TestChannel. The channel is expected to exist otherwise testing will fail.

func (*TestChain) GetChannelCapability

func (chain *TestChain) GetChannelCapability(portID, channelID string) *capabilitytypes.Capability

GetChannelCapability returns the channel capability for the given portID and channelID. The capability must exist, otherwise testing will fail.

func (*TestChain) GetClientState

func (chain *TestChain) GetClientState(clientID string) exported.ClientState

GetClientState retrieves the client state for the provided clientID. The client is expected to exist otherwise testing will fail.

func (*TestChain) GetConnection

func (chain *TestChain) GetConnection(testConnection *TestConnection) connectiontypes.ConnectionEnd

GetConnection retrieves an IBC Connection for the provided TestConnection. The connection is expected to exist otherwise testing will fail.

func (*TestChain) GetConsensusState

func (chain *TestChain) GetConsensusState(clientID string, height exported.Height) (exported.ConsensusState, bool)

GetConsensusState retrieves the consensus state for the provided clientID and height. It will return a success boolean depending on if consensus state exists or not.

func (*TestChain) GetContext

func (chain *TestChain) GetContext() sdk.Context

func (*TestChain) GetFabricContext

func (chain *TestChain) GetFabricContext() *contractapi.TransactionContext

func (TestChain) GetLastHeader

func (chain TestChain) GetLastHeader() *ibctmtypes.Header

func (*TestChain) GetPacketData

func (chain *TestChain) GetPacketData(counterparty TestChainI) []byte

GetPacketData returns a ibc-transfer marshalled packet to be used for callback testing.

func (*TestChain) GetPortCapability

func (chain *TestChain) GetPortCapability(portID string) *capabilitytypes.Capability

GetPortCapability returns the port capability for the given portID. The capability must exist, otherwise testing will fail.

func (*TestChain) GetPrefix

func (chain *TestChain) GetPrefix() commitmenttypes.MerklePrefix

GetPrefix returns the prefix for used by a chain in connection creation

func (TestChain) GetSenderAccount

func (chain TestChain) GetSenderAccount() authtypes.AccountI

func (*TestChain) NewClientID

func (chain *TestChain) NewClientID(clientType string) string

NewClientID appends a new clientID string in the format: ClientFor<counterparty-chain-id><index>

func (*TestChain) NewFabricClientState

func (chain *TestChain) NewFabricClientState(counterparty TestChainI, clientID string) *fabrictypes.ClientState

func (*TestChain) NewFabricConsensusState

func (chain *TestChain) NewFabricConsensusState(counterparty TestChainI) *fabrictypes.ConsensusState

func (*TestChain) NextBlock

func (chain *TestChain) NextBlock()

func (*TestChain) NextTestChannel

func (chain *TestChain) NextTestChannel(conn *TestConnection, portID string) TestChannel

NextTestChannel returns the next test channel to be created on this connection, but does not add it to the list of created channels. This function is expected to be used when the caller has not created the associated channel in app state, but would still like to refer to the non-existent channel usually to test for its non-existence.

channel ID format: <connectionid>-chan<channel-index>

The port is passed in by the caller.

func (*TestChain) QueryClientStateProof

func (chain *TestChain) QueryClientStateProof(clientID string) (exported.ClientState, []byte)

QueryClientStateProof performs and abci query for a client state stored with a given clientID and returns the ClientState along with the proof

func (*TestChain) QueryConsensusStateProof

func (chain *TestChain) QueryConsensusStateProof(clientID string) ([]byte, clienttypes.Height)

QueryConsensusStateProof performs an abci query for a consensus state stored on the given clientID. The proof and consensusHeight are returned.

func (*TestChain) QueryProof

func (chain *TestChain) QueryProof(key []byte) ([]byte, clienttypes.Height)

QueryProof performs an abci query with the given key and returns the proto encoded merkle proof for the query and the height at which the proof will succeed on a tendermint verifier.

func (*TestChain) SendMsgs

func (chain *TestChain) SendMsgs(msgs ...sdk.Msg) (*sdk.Result, error)

SendMsgs delivers a transaction through the application. It updates the senders sequence number and updates the TestChain's headers. It returns the result and error if one occurred.

func (*TestChain) SendPacket

func (chain *TestChain) SendPacket(
	packet exported.PacketI,
) error

SendPacket simulates sending a packet through the channel keeper. No message needs to be passed since this call is made from a module.

func (TestChain) Type

func (chain TestChain) Type() string

Type implements TestChainI.Type

func (*TestChain) UpdateClient

func (chain *TestChain) UpdateClient(counterparty TestChainI, clientID string) error

TODO add tests for other headers UpdateClient updates the sequence and timestamp

func (*TestChain) WriteAcknowledgement

func (chain *TestChain) WriteAcknowledgement(
	packet exported.PacketI,
) error

WriteAcknowledgement simulates writing an acknowledgement to the chain.

type TestChainI

type TestChainI interface {
	Type() string
	GetApp() interface{}
	GetChainID() string
	GetSenderAccount() authtypes.AccountI
	GetLastHeader() *ibctmtypes.Header
	NextBlock()
	GetContext() sdk.Context

	AddTestConnection(clientID, counterpartyClientID, nextChannelVersion string) *TestConnection
	AddTestChannel(conn *TestConnection, portID string) TestChannel
	ConstructNextTestConnection(clientID, counterpartyClientID, nextChannelVersion string) *TestConnection

	GetChannel(testChannel TestChannel) channeltypes.Channel

	ConnectionOpenInit(
		counterparty TestChainI,
		connection, counterpartyConnection *TestConnection,
	) error
	ConnectionOpenTry(
		counterparty TestChainI,
		connection, counterpartyConnection *TestConnection,
	) error
	ConnectionOpenAck(
		counterparty TestChainI,
		connection, counterpartyConnection *TestConnection,
	) error
	ConnectionOpenConfirm(
		counterparty TestChainI,
		connection, counterpartyConnection *TestConnection,
	) error

	ChanOpenInit(
		ch, counterparty TestChannel,
		order channeltypes.Order,
		connectionID string,
	) error
	ChanOpenTry(
		counterparty TestChainI,
		ch, counterpartyCh TestChannel,
		order channeltypes.Order,
		connectionID string,
	) error
	ChanOpenAck(
		counterparty TestChainI,
		ch, counterpartyCh TestChannel,
	) error
	ChanOpenConfirm(
		counterparty TestChainI,
		ch, counterpartyCh TestChannel,
	) error
	ChanCloseInit(
		counterparty TestChainI,
		channel TestChannel,
	) error

	CreatePortCapability(portID string)

	QueryClientStateProof(clientID string) (exported.ClientState, []byte)
	QueryProof(key []byte) ([]byte, clienttypes.Height)
	QueryConsensusStateProof(clientID string) ([]byte, clienttypes.Height)

	NewClientID(clientType string) string
	GetPrefix() commitmenttypes.MerklePrefix

	SendMsgs(msgs ...sdk.Msg) (*sdk.Result, error)
}

type TestChannel

type TestChannel struct {
	PortID               string
	ID                   string
	ClientID             string
	CounterpartyClientID string
	Version              string
}

TestChannel is a testing helper struct to keep track of the portID and channelID used in creating and interacting with a channel. The clientID and counterparty client ID are also tracked to cut down on querying and argument passing.

type TestConnection

type TestConnection struct {
	ID                   string
	ClientID             string
	CounterpartyClientID string
	NextChannelVersion   string
	Channels             []TestChannel
}

TestConnection is a testing helper struct to keep track of the connectionID, source clientID, counterparty clientID, and the next channel version used in creating and interacting with a connection.

func (*TestConnection) FirstOrNextTestChannel

func (conn *TestConnection) FirstOrNextTestChannel(portID string) TestChannel

FirstOrNextTestChannel returns the first test channel if it exists, otherwise it returns the next test channel to be created. This function is expected to be used when the caller does not know if the channel has or has not been created in app state, but would still like to refer to it to test existence or non-existence.

type TxSignMode

type TxSignMode uint8
const (
	TxSignModeStdTx TxSignMode = iota + 1
	TxSignModeFabricTx
)

Jump to

Keyboard shortcuts

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