Merge pull request #653 from cosmos/query-client-proof-helper
Query client proof helper
This commit is contained in:
commit
ecb6f7f67f
@ -6,6 +6,8 @@ import { Client as TendermintClient, Header, NewBlockHeaderEvent, ProofOp } from
|
||||
import { arrayContentEquals, assert, assertDefined, isNonNullObject, sleep } from "@cosmjs/utils";
|
||||
import { Stream } from "xstream";
|
||||
|
||||
import { ProofOps } from "../codec/tendermint/crypto/proof";
|
||||
|
||||
type QueryExtensionSetup<P> = (base: QueryClient) => P;
|
||||
|
||||
function checkAndParseOp(op: ProofOp, kind: string, key: Uint8Array): ics23.CommitmentProof {
|
||||
@ -18,6 +20,13 @@ function checkAndParseOp(op: ProofOp, kind: string, key: Uint8Array): ics23.Comm
|
||||
return ics23.CommitmentProof.decode(op.data);
|
||||
}
|
||||
|
||||
export interface ProvenQuery {
|
||||
readonly key: Uint8Array;
|
||||
readonly value: Uint8Array;
|
||||
readonly proof: ProofOps;
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export class QueryClient {
|
||||
/** Constructs a QueryClient with 0 extensions */
|
||||
public static withExtensions(tmClient: TendermintClient): QueryClient;
|
||||
@ -158,62 +167,73 @@ export class QueryClient {
|
||||
}
|
||||
|
||||
public async queryVerified(store: string, key: Uint8Array): Promise<Uint8Array> {
|
||||
const response = await this.tmClient.abciQuery({
|
||||
const response = await this.queryRawProof(store, key);
|
||||
|
||||
const subProof = checkAndParseOp(response.proof.ops[0], "ics23:iavl", key);
|
||||
const storeProof = checkAndParseOp(response.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 (!response.value || response.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, key);
|
||||
} 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, key, response.value);
|
||||
}
|
||||
|
||||
// the storeproof must map it's declared value (root of subProof) to the appHash of the next block
|
||||
const header = await this.getNextHeader(response.height);
|
||||
verifyExistence(storeProof.exist, tendermintSpec, header.appHash, toAscii(store), storeProof.exist.value);
|
||||
|
||||
return response.value;
|
||||
}
|
||||
|
||||
public async queryRawProof(store: string, queryKey: Uint8Array): Promise<ProvenQuery> {
|
||||
const { key, value, height, proof, code, log } = await this.tmClient.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: key,
|
||||
data: queryKey,
|
||||
prove: true,
|
||||
});
|
||||
|
||||
if (response.code) {
|
||||
throw new Error(`Query failed with (${response.code}): ${response.log}`);
|
||||
if (code) {
|
||||
throw new Error(`Query failed with (${code}): ${log}`);
|
||||
}
|
||||
|
||||
if (!arrayContentEquals(response.key, key)) {
|
||||
throw new Error(`Response key ${toHex(response.key)} doesn't match query key ${toHex(key)}`);
|
||||
if (!arrayContentEquals(queryKey, key)) {
|
||||
throw new Error(`Response key ${toHex(key)} doesn't match query key ${toHex(queryKey)}`);
|
||||
}
|
||||
|
||||
if (response.proof) {
|
||||
if (response.proof.ops.length !== 2) {
|
||||
throw new Error(
|
||||
`Expected 2 proof ops, got ${response.proof?.ops.length ?? 0}. Are you using stargate?`,
|
||||
);
|
||||
}
|
||||
|
||||
const subProof = checkAndParseOp(response.proof.ops[0], "ics23:iavl", key);
|
||||
const storeProof = checkAndParseOp(response.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 (!response.value || response.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, key);
|
||||
} 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, key, response.value);
|
||||
}
|
||||
|
||||
// the storeproof must map it's declared value (root of subProof) to the appHash of the next block
|
||||
const header = await this.getNextHeader(response.height);
|
||||
verifyExistence(
|
||||
storeProof.exist,
|
||||
tendermintSpec,
|
||||
header.appHash,
|
||||
toAscii(store),
|
||||
storeProof.exist.value,
|
||||
);
|
||||
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?`);
|
||||
}
|
||||
|
||||
return response.value;
|
||||
// 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,
|
||||
value,
|
||||
height,
|
||||
// need to clone this: readonly input / writeable output
|
||||
proof: {
|
||||
ops: [...proof.ops],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async queryUnverified(path: string, request: Uint8Array): Promise<Uint8Array> {
|
||||
|
||||
@ -149,11 +149,19 @@ export class StargateClient {
|
||||
return status.syncInfo.latestBlockHeight;
|
||||
}
|
||||
|
||||
// this is nice to display data to the user, but is slower
|
||||
public async getAccount(searchAddress: string): Promise<Account | null> {
|
||||
const account = await this.queryClient.auth.account(searchAddress);
|
||||
return account ? accountFromProto(account) : null;
|
||||
}
|
||||
|
||||
// if we just need to get the sequence for signing a transaction, let's make this faster
|
||||
// (no need to wait a block before submitting)
|
||||
public async getAccountUnverified(searchAddress: string): Promise<Account | null> {
|
||||
const account = await this.queryClient.auth.unverified.account(searchAddress);
|
||||
return account ? accountFromProto(account) : null;
|
||||
}
|
||||
|
||||
public async getSequence(address: string): Promise<SequenceResponse | null> {
|
||||
const account = await this.getAccount(address);
|
||||
if (account) {
|
||||
|
||||
@ -71,7 +71,7 @@ interface RpcAbciQueryResponse {
|
||||
readonly key: string;
|
||||
/** base64 encoded */
|
||||
readonly value?: string;
|
||||
readonly proof?: RpcQueryProof;
|
||||
readonly proofOps?: RpcQueryProof;
|
||||
readonly height?: string;
|
||||
readonly index?: string;
|
||||
readonly code?: string; // only for errors
|
||||
@ -82,7 +82,7 @@ function decodeAbciQuery(data: RpcAbciQueryResponse): responses.AbciQueryRespons
|
||||
return {
|
||||
key: fromBase64(optional(data.key, "")),
|
||||
value: fromBase64(optional(data.value, "")),
|
||||
proof: may(decodeQueryProof, data.proof),
|
||||
proof: may(decodeQueryProof, data.proofOps),
|
||||
height: may(Integer.parse, data.height),
|
||||
code: may(Integer.parse, data.code),
|
||||
index: may(Integer.parse, data.index),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user