Skip to content

Commit

Permalink
Remove support for verified queries
Browse files Browse the repository at this point in the history
The ics23 library for TypeScript is unmaintained sucht that we cannot
provide this feature anymore
  • Loading branch information
webmaster128 committed Jan 15, 2025
1 parent 9f92a18 commit e6e4c24
Show file tree
Hide file tree
Showing 3 changed files with 2 additions and 259 deletions.
53 changes: 0 additions & 53 deletions packages/stargate/src/modules/ibc/queries.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { toAscii } from "@cosmjs/encoding";
import { Uint64 } from "@cosmjs/math";
import { Any } from "cosmjs-types/google/protobuf/any";
import {
QueryClientImpl as TransferQuery,
QueryDenomTraceResponse,
QueryDenomTracesResponse,
QueryParamsResponse as QueryTransferParamsResponse,
} from "cosmjs-types/ibc/applications/transfer/v1/query";
import { Channel } from "cosmjs-types/ibc/core/channel/v1/channel";
import {
QueryChannelClientStateResponse,
QueryChannelConsensusStateResponse,
Expand Down Expand Up @@ -166,22 +163,6 @@ export interface IbcExtension {
readonly allDenomTraces: () => Promise<QueryDenomTracesResponse>;
readonly params: () => Promise<QueryTransferParamsResponse>;
};
readonly verified: {
readonly channel: {
readonly channel: (portId: string, channelId: string) => Promise<Channel | null>;
readonly packetCommitment: (
portId: string,
channelId: string,
sequence: number,
) => Promise<Uint8Array>;
readonly packetAcknowledgement: (
portId: string,
channelId: string,
sequence: number,
) => Promise<Uint8Array>;
readonly nextSequenceReceive: (portId: string, channelId: string) => Promise<number | null>;
};
};
};
}

Expand Down Expand Up @@ -502,40 +483,6 @@ export function setupIbcExtension(base: QueryClient): IbcExtension {
},
params: async () => transferQueryService.Params({}),
},
verified: {
channel: {
channel: async (portId: string, channelId: string) => {
// keeper: https://github.com/cosmos/cosmos-sdk/blob/3bafd8255a502e5a9cee07391cf8261538245dfd/x/ibc/04-channel/keeper/keeper.go#L55-L65
// key: https://github.com/cosmos/cosmos-sdk/blob/ef0a7344af345882729598bc2958a21143930a6b/x/ibc/24-host/keys.go#L117-L120
const key = toAscii(`channelEnds/ports/${portId}/channels/${channelId}`);
const { value } = await base.queryStoreVerified("ibc", key);
return value.length ? Channel.decode(value) : null;
},
packetCommitment: async (portId: string, channelId: string, sequence: number) => {
// keeper: https://github.com/cosmos/cosmos-sdk/blob/3bafd8255a502e5a9cee07391cf8261538245dfd/x/ibc/04-channel/keeper/keeper.go#L128-L133
// key: https://github.com/cosmos/cosmos-sdk/blob/ef0a7344af345882729598bc2958a21143930a6b/x/ibc/24-host/keys.go#L183-L185
const key = toAscii(`commitments/ports/${portId}/channels/${channelId}/packets/${sequence}`);
const { value } = await base.queryStoreVerified("ibc", key);
// keeper code doesn't parse, but returns raw
return value;
},
packetAcknowledgement: async (portId: string, channelId: string, sequence: number) => {
// keeper: https://github.com/cosmos/cosmos-sdk/blob/3bafd8255a502e5a9cee07391cf8261538245dfd/x/ibc/04-channel/keeper/keeper.go#L159-L166
// key: https://github.com/cosmos/cosmos-sdk/blob/ef0a7344af345882729598bc2958a21143930a6b/x/ibc/24-host/keys.go#L153-L156
const key = toAscii(`acks/ports/${portId}/channels/${channelId}/acknowledgements/${sequence}`);
const { value } = await base.queryStoreVerified("ibc", key);
// keeper code doesn't parse, but returns raw
return value;
},
nextSequenceReceive: async (portId: string, channelId: string) => {
// keeper: https://github.com/cosmos/cosmos-sdk/blob/3bafd8255a502e5a9cee07391cf8261538245dfd/x/ibc/04-channel/keeper/keeper.go#L92-L101
// key: https://github.com/cosmos/cosmos-sdk/blob/ef0a7344af345882729598bc2958a21143930a6b/x/ibc/24-host/keys.go#L133-L136
const key = toAscii(`seqAcks/ports/${portId}/channels/${channelId}/nextSequenceAck`);
const { value } = await base.queryStoreVerified("ibc", key);
return value.length ? Uint64.fromBytes(value).toNumber() : null;
},
},
},
},
};
}
65 changes: 0 additions & 65 deletions packages/stargate/src/queryclient/queryclient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { coin } from "@cosmjs/amino";
import { toAscii } from "@cosmjs/encoding";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { CometClient, Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { assert } from "@cosmjs/utils";
import { Metadata } from "cosmjs-types/cosmos/bank/v1beta1/bank";
import {
QueryAllBalancesRequest,
QueryAllBalancesResponse,
Expand All @@ -19,7 +17,6 @@ import {
makeRandomAddress,
pendingWithoutSimapp,
simapp,
simapp44Enabled,
unused,
} from "../testutils.spec";
import { QueryClient } from "./queryclient";
Expand All @@ -29,69 +26,7 @@ async function makeClient(rpcUrl: string): Promise<[QueryClient, CometClient]> {
return [QueryClient.withExtensions(cometClient), cometClient];
}

/**
* See
* - https://github.com/cosmos/cosmos-sdk/blob/v0.42.10/x/bank/types/key.go#L27
* - https://github.com/cosmos/cosmos-sdk/blob/v0.44.2/x/bank/types/key.go#L28
*/
const denomMetadataPrefix = new Uint8Array([0x01]);

describe("QueryClient", () => {
describe("queryStoreVerified", () => {
it("works via WebSockets", async () => {
pendingWithoutSimapp();
const [client, cometClient] = await makeClient(simapp.tendermintUrlWs);

// "keys before 0.45 had denom two times in the key"
// https://github.com/cosmos/cosmos-sdk/blob/10ad61a4dd/x/bank/migrations/v045/store_test.go#L91
let queryKey: Uint8Array;
if (simapp44Enabled()) {
queryKey = Uint8Array.from([
...denomMetadataPrefix,
...toAscii(simapp.denomFee),
...toAscii(simapp.denomFee),
]);
} else {
queryKey = Uint8Array.from([...denomMetadataPrefix, ...toAscii(simapp.denomFee)]);
}
const { key, value, height } = await client.queryStoreVerified("bank", queryKey);
expect(height).toBeGreaterThanOrEqual(1);
expect(key).toEqual(queryKey);
const response = Metadata.decode(value);
expect(response.base).toEqual(simapp.denomFee);
expect(response.description).toEqual("The fee token of this test chain");

cometClient.disconnect();
});

it("works via http", async () => {
pendingWithoutSimapp();
const [client, cometClient] = await makeClient(simapp.tendermintUrlHttp);

// "keys before 0.45 had denom two times in the key"
// https://github.com/cosmos/cosmos-sdk/blob/10ad61a4dd/x/bank/migrations/v045/store_test.go#L91
let queryKey: Uint8Array;
if (simapp44Enabled()) {
queryKey = Uint8Array.from([
...denomMetadataPrefix,
...toAscii(simapp.denomFee),
...toAscii(simapp.denomFee),
]);
} else {
queryKey = Uint8Array.from([...denomMetadataPrefix, ...toAscii(simapp.denomFee)]);
}

const { key, value, height } = await client.queryStoreVerified("bank", queryKey);
expect(height).toBeGreaterThanOrEqual(1);
expect(key).toEqual(queryKey);
const response = Metadata.decode(value);
expect(response.base).toEqual(simapp.denomFee);
expect(response.description).toEqual("The fee token of this test chain");

cometClient.disconnect();
});
});

describe("queryAbci", () => {
it("works via WebSockets", async () => {
pendingWithoutSimapp();
Expand Down
143 changes: 2 additions & 141 deletions packages/stargate/src/queryclient/queryclient.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
/* eslint-disable no-dupe-class-members, @typescript-eslint/ban-types, @typescript-eslint/naming-convention */
import { iavlSpec, ics23, tendermintSpec, verifyExistence, verifyNonExistence } from "@confio/ics23";
import { toAscii, toHex } from "@cosmjs/encoding";
import { firstEvent } from "@cosmjs/stream";
import { CometClient, tendermint34 } from "@cosmjs/tendermint-rpc";
import { arrayContentEquals, assert, assertDefined, isNonNullObject, sleep } from "@cosmjs/utils";
import { CometClient } from "@cosmjs/tendermint-rpc";
import { assert, isNonNullObject } from "@cosmjs/utils";
import { ProofOps } from "cosmjs-types/tendermint/crypto/proof";
import { Stream } from "xstream";

type QueryExtensionSetup<P> = (base: QueryClient) => P;

function checkAndParseOp(op: tendermint34.ProofOp, kind: string, key: Uint8Array): ics23.CommitmentProof {
if (op.type !== kind) {
throw new Error(`Op expected to be ${kind}, got "${op.type}`);
}
if (!arrayContentEquals(key, op.key)) {
throw new Error(`Proven key different than queried key.\nQuery: ${toHex(key)}\nProven: ${toHex(op.key)}`);
}
return ics23.CommitmentProof.decode(op.data);
}

export interface ProvenQuery {
readonly key: Uint8Array;
readonly value: Uint8Array;
Expand Down Expand Up @@ -512,90 +498,6 @@ export class QueryClient {
this.cometClient = cometClient;
}

/**
* Queries the database store with a proof, which is then verified.
*
* Please note: the current implementation trusts block headers it gets from the PRC endpoint.
*/
public async queryStoreVerified(
store: string,
queryKey: Uint8Array,
desiredHeight?: number,
): Promise<QueryStoreResponse> {
const { height, proof, key, value } = await this.queryRawProof(store, queryKey, desiredHeight);

const subProof = checkAndParseOp(proof.ops[0], "ics23:iavl", queryKey);
const storeProof = checkAndParseOp(proof.ops[1], "ics23:simple", toAscii(store));

// this must always be existence, if the store is not a typo
assert(storeProof.exist);
assert(storeProof.exist.value);

// this may be exist or non-exist, depends on response
if (!value || value.length === 0) {
// non-existence check
assert(subProof.nonexist);
// the subproof must map the desired key to the "value" of the storeProof
verifyNonExistence(subProof.nonexist, iavlSpec, storeProof.exist.value, queryKey);
} else {
// existence check
assert(subProof.exist);
assert(subProof.exist.value);
// the subproof must map the desired key to the "value" of the storeProof
verifyExistence(subProof.exist, iavlSpec, storeProof.exist.value, queryKey, value);
}

// the store proof must map its declared value (root of subProof) to the appHash of the next block
const header = await this.getNextHeader(height);
verifyExistence(storeProof.exist, tendermintSpec, header.appHash, toAscii(store), storeProof.exist.value);

return { key, value, height };
}

public async queryRawProof(
store: string,
queryKey: Uint8Array,
desiredHeight?: number,
): Promise<ProvenQuery> {
const { key, value, height, proof, code, log } = await this.cometClient.abciQuery({
// we need the StoreKey for the module, not the module name
// https://github.com/cosmos/cosmos-sdk/blob/8cab43c8120fec5200c3459cbf4a92017bb6f287/x/auth/types/keys.go#L12
path: `/store/${store}/key`,
data: queryKey,
prove: true,
height: desiredHeight,
});

if (code) {
throw new Error(`Query failed with (${code}): ${log}`);
}

if (!arrayContentEquals(queryKey, key)) {
throw new Error(`Response key ${toHex(key)} doesn't match query key ${toHex(queryKey)}`);
}

if (!height) {
throw new Error("No query height returned");
}
if (!proof || proof.ops.length !== 2) {
throw new Error(`Expected 2 proof ops, got ${proof?.ops.length ?? 0}. Are you using stargate?`);
}

// we don't need the results, but we can ensure the data is the proper format
checkAndParseOp(proof.ops[0], "ics23:iavl", key);
checkAndParseOp(proof.ops[1], "ics23:simple", toAscii(store));

return {
key: key,
value: value,
height: height,
// need to clone this: readonly input / writeable output
proof: {
ops: [...proof.ops],
},
};
}

/**
* Performs an ABCI query to Tendermint without requesting a proof.
*
Expand Down Expand Up @@ -628,45 +530,4 @@ export class QueryClient {
height: response.height,
};
}

// this must return the header for height+1
// throws an error if height is 0 or undefined
private async getNextHeader(height?: number): Promise<tendermint34.Header> {
assertDefined(height);
if (height === 0) {
throw new Error("Query returned height 0, cannot prove it");
}

const searchHeight = height + 1;
let nextHeader: tendermint34.Header | undefined;
let headersSubscription: Stream<tendermint34.NewBlockHeaderEvent> | undefined;
try {
headersSubscription = this.cometClient.subscribeNewBlockHeader();
} catch {
// Ignore exception caused by non-WebSocket Tendermint clients
}

if (headersSubscription) {
const firstHeader = await firstEvent(headersSubscription);
// The first header we get might not be n+1 but n+2 or even higher. In such cases we fall back on a query.
if (firstHeader.height === searchHeight) {
nextHeader = firstHeader;
}
}

while (!nextHeader) {
// start from current height to avoid backend error for minHeight in the future
const correctHeader = (await this.cometClient.blockchain(height, searchHeight)).blockMetas
.map((meta) => meta.header)
.find((h) => h.height === searchHeight);
if (correctHeader) {
nextHeader = correctHeader;
} else {
await sleep(1000);
}
}

assert(nextHeader.height === searchHeight, "Got wrong header. This is a bug in the logic above.");
return nextHeader;
}
}

0 comments on commit e6e4c24

Please sign in to comment.