ics | title | stage | category | requires | kind | author | created | modified |
---|---|---|---|---|---|---|---|---|
TBD |
Interchain Queries |
Draft |
IBC/APP |
25, 26 |
instantiation |
Ali Zahid Raja <ali@polymerlabs.org>, Ehsan Saradar <ehsan@quasar.fi> |
2022-06-22 |
2022-08-30 |
This document serves as a guide for better understanding the interchain queries implementation that uses ABCI Queries.
Interchain Queries enable blockchains to query the state of an account on another chain without the need for ICA auth. ICS-27, Interchain Accounts, is used for IBC transactions, e.g. to transfer coins from one interchain account to another, whereas Interchain Queries are used for IBC Queries, e.g. to query the balance of an account on another chain. In short, ICA is cross chain writes while ICQ is cross chain reads.
Host Chain
: The chain where the query is sent. The host chain listens for IBC packets from a controller chain which contain instructions describing an ABCI Query Request.Controller Chain
: The chain sending the query to the host chain. The controller chain sends IBC packets to the host chain to query information.Interchain Query
: An IBC packet that contains information about the query in the form of an ABCI RequestQuery.
The chain which sends the query becomes the controller chain, and the chain which receives the query and responds becomes the host chain.
- Permissionless: An interchain query may be created by any actor without the approval of a third party (e.g. chain governance)
ABCI RequestQuery enables blockchains to request information made by the end-users of applications. A query is received by a full node through its consensus engine and relayed to the application via the ABCI. It is then routed to the appropriate module via BaseApp's query router so that it can be processed by the module's query service.
ICQ can only return information from stale reads, for a read that requires consensus, ICA (ICS-27) will be used.
SendQuery
is used to send an IBC packet containing query information to an interchain account on a host chain.
func (k Keeper) SendQuery(ctx sdk.Context, sourcePort, sourceChannel string, chanCap *capabilitytypes.Capability,
reqs []abci.RequestQuery, timeoutHeight clienttypes.Height, timeoutTimestamp uint64) (uint64, error) {
sourceChannelEnd, found := k.channelKeeper.GetChannel(ctx, sourcePort, sourceChannel)
if !found {
return 0, sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", sourcePort, sourceChannel)
}
destinationPort := sourceChannelEnd.GetCounterparty().GetPortID()
destinationChannel := sourceChannelEnd.GetCounterparty().GetChannelID()
icqPacketData := types.InterchainQueryPacketData{
Requests: reqs,
}
return k.createOutgoingPacket(ctx, sourcePort, sourceChannel, destinationPort, destinationChannel, chanCap, icqPacketData, timeoutTimestamp)
}
authenticateQuery
is called before executeQuery
.
authenticateQuery
checks that the query is a part of the whitelisted queries.
func (k Keeper) authenticateQuery(ctx sdk.Context, q abci.RequestQuery) error {
allowQueries := k.GetAllowQueries(ctx)
if !types.ContainsQueryPath(allowQueries, q.Path) {
return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "query path not allowed: %s", q.Path)
}
if !(q.Height == 0 || q.Height == ctx.BlockHeight()) {
return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "query height not allowed: %d", q.Height)
}
if q.Prove {
return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "query proof not allowed")
}
return nil
}
Executes each query sent by the controller chain.
func (k Keeper) executeQuery(ctx sdk.Context, reqs []abci.RequestQuery) ([]byte, error) {
resps := make([]abci.ResponseQuery, len(reqs))
for i, req := range reqs {
if err := k.authenticateQuery(ctx, req); err != nil {
return nil, err
}
resp := k.querier.Query(req)
// Remove non-deterministic fields from response
resps[i] = abci.ResponseQuery{
Code: resp.Code,
Index: resp.Index,
Key: resp.Key,
Value: resp.Value,
Height: resp.Height,
}
}
bz, err := types.SerializeCosmosResponse(resps)
if err != nil {
return nil, err
}
ack := types.InterchainQueryPacketAck{
Data: bz,
}
data, err := types.ModuleCdc.MarshalJSON(&ack)
if err != nil {
return nil, sdkerrors.Wrap(err, "failed to marshal tx data")
}
return data, nil
}
InterchainQueryPacketData
is comprised of a raw query.
message InterchainQueryPacketData {
bytes data = 1;
}
InterchainQueryPacketAck
is comprised of an ABCI query response with non-deterministic fields left empty (e.g. Codespace, Log, Info).
message InterchainQueryPacketAck {
bytes data = 1;
}
CosmosQuery
contains a list of tendermint ABCI query requests. It should be used when sending queries to an SDK host chain.
message CosmosQuery {
repeated tendermint.abci.RequestQuery requests = 1 [(gogoproto.nullable) = false];
}
CosmosResponse
contains a list of tendermint ABCI query responses. It should be used when receiving responses from an SDK host chain.
message CosmosResponse {
repeated tendermint.abci.ResponseQuery responses = 1 [(gogoproto.nullable) = false];
}
onRecvPacket
is called by the routing module when a packet addressed to this module has been received.
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet) ([]byte, error) {
var data types.InterchainQueryPacketData
if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
// UnmarshalJSON errors are indeterminate and therefore are not wrapped and included in failed acks
return nil, errors.Wrapf(types.ErrUnknownDataType, "cannot unmarshal ICQ packet data")
}
reqs, err := types.DeserializeCosmosQuery(data.GetData())
if err != nil {
return nil, err
}
// If we panic when executing a query it should be returned as an error.
var response []byte
err = applyFuncIfNoError(ctx, func(ctx sdk.Context) error {
response, err = k.executeQuery(ctx, reqs)
return err
})
if err != nil {
return nil, err
}
return response, err
}
To get the balance of an address we can use the following In order to send a packet from a module, first you need to prepare your query Data and encapsule it in an ABCI RequestQuery. Then you can use SerializeCosmosQuery to construct the Data of InterchainQueryPacketData packet data. After these steps you should use ICS 4 wrapper to send your packet to the host chain through a valid channel.
q := banktypes.QueryAllBalancesRequest{
Address: "cosmos1tshnze3yrtv3hk9x536p7znpxeckd4v9ha0trg",
Pagination: &query.PageRequest{
Offset: 0,
Limit: 10,
},
}
reqs := []abcitypes.RequestQuery{
{
Path: "/cosmos.bank.v1beta1.Query/AllBalances",
Data: k.cdc.MustMarshal(&q),
},
}
bz, err := icqtypes.SerializeCosmosQuery(reqs)
if err != nil {
return 0, err
}
icqPacketData := icqtypes.InterchainQueryPacketData{
Data: bz,
}
packet := channeltypes.NewPacket(
icqPacketData.GetBytes(),
sequence,
sourcePort,
sourceChannel,
destinationPort,
destinationChannel,
clienttypes.ZeroHeight(),
timeoutTimestamp,
)
// Send the `packet` with ICS-4 interface
Acknowledgements are written to the host chain's state on a successful OnRecvPacket
and then the controller chain verifies that
acknowledgement in OnAcknowledgement
. It is here that developers should implement there custom handling logic for the various
expected responses from the queries.
A successful acknowledgment will be sent back to the querier module as an InterchainQueryPacketAck
.
The Data
field should be deserialized to an array of ABCI ResponseQuery with the DeserializeCosmosResponse
function.
switch resp := ack.Response.(type) {
case *channeltypes.Acknowledgement_Result:
var ackData icqtypes.InterchainQueryPacketAck
if err := icqtypes.ModuleCdc.UnmarshalJSON(resp.Result, &ackData); err != nil {
return sdkerrors.Wrap(err, "failed to unmarshal interchain query packet ack")
}
resps, err := icqtypes.DeserializeCosmosResponse(ackData.Data)
if err != nil {
return sdkerrors.Wrap(err, "failed to unmarshal interchain query packet ack to cosmos response")
}
if len(resps) < 1 {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "no responses in interchain query packet ack")
}
var r banktypes.QueryAllBalancesResponse
if err := k.cdc.Unmarshal(resps[0].Value, &r); err != nil {
return sdkerrors.Wrapf(err, "failed to unmarshal interchain query response to type %T", resp)
}
// `r` is the response of your query
...
Another implementation of Interchain Queries is by the use of the KV store which can be seen implemented here by QuickSilver.
The implementation works even if the host side hasn't implemented ICQ, however, it does not fully leverage the IBC standards.
June 22, 2022 - Draft
August 30, 2022 - Major Revisions, added ICQ to IBC-test