Merge pull request #839 from cosmos/backport-add-block-search

Backport block search in Tendermint 0.34 client
This commit is contained in:
Simon Warta 2021-06-23 16:35:16 +02:00 committed by GitHub
commit a13ceb6fc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 204 additions and 21 deletions

View File

@ -6,6 +6,16 @@ and this project adheres to
## [Unreleased]
### Added
- @cosmjs/tendermint-rpc: `Tendermint34Client.blockSearch` and
`Tendermint34Client.blockSearchAll` were added to allow searching blocks in
Tendermint 0.34.9+ backends. This is a backport of [#815]. Note: Decoding
blocks of height 1 is unsupported. This is fixed in [#815] and will be
released as part of CosmJS 0.26.
[#815]: https://github.com/cosmos/cosmjs/pull/815
## [0.25.4] - 2021-05-31
### Fixed

View File

@ -101,6 +101,13 @@ discussion please reach out to the team.
## Known limitations
### 0.25
1. Decoding blocks of height 1 is unsupported. This is fixed in [#815] and will
be released as part of CosmJS 0.26.
[#815]: https://github.com/cosmos/cosmjs/pull/815
### 0.24
1. `AuthExtension` and all higher level Stargate clients only support

View File

@ -28,23 +28,15 @@ export const adaptor33 = v0_33;
*/
export const adaptor34 = v0_33; // With this alias we can swap out the implementation without affecting caller code.
const hashes = {
v0_34: [
"ca2c9df", // v0.34.0-rc6
"182fa32", // v0.34.0
],
};
/**
* Returns an Adaptor implementation for a given tendermint version.
* Throws when version is not supported.
*
* @param version full Tendermint version string, e.g. "0.20.1"
*/
export function adaptorForVersion(version: string): Adaptor {
if (version.startsWith("0.33.") || version.startsWith("0.34.") || hashes.v0_34.includes(version)) {
return v0_33;
} else {
throw new Error(`Unsupported tendermint version: ${version}`);
}
export function adaptorForVersion(_version: string): Adaptor {
// Note: In some cases, Tendermint 0.34 returns an empty version value.
// This supports 0.33 and 0.34 now, no matter which version you provide.
// Very soon this function becomes obsolete (https://github.com/cosmos/cosmjs/issues/789).
return v0_33;
}

View File

@ -167,7 +167,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor, expecte
const status = await client.status();
// node info
expect(status.nodeInfo.version).toEqual(expected.version);
expect(status.nodeInfo.version).toMatch(expected.version);
expect(status.nodeInfo.protocolVersion).toEqual({
p2p: expected.p2pVersion,
block: expected.blockVersion,

View File

@ -23,6 +23,7 @@ export interface Params {
readonly encodeBlock: (req: requests.BlockRequest) => JsonRpcRequest;
readonly encodeBlockchain: (req: requests.BlockchainRequest) => JsonRpcRequest;
readonly encodeBlockResults: (req: requests.BlockResultsRequest) => JsonRpcRequest;
readonly encodeBlockSearch: (req: requests.BlockSearchRequest) => JsonRpcRequest;
readonly encodeBroadcastTx: (req: requests.BroadcastTxRequest) => JsonRpcRequest;
readonly encodeCommit: (req: requests.CommitRequest) => JsonRpcRequest;
readonly encodeGenesis: (req: requests.GenesisRequest) => JsonRpcRequest;
@ -39,6 +40,7 @@ export interface Responses {
readonly decodeAbciQuery: (response: JsonRpcSuccessResponse) => responses.AbciQueryResponse;
readonly decodeBlock: (response: JsonRpcSuccessResponse) => responses.BlockResponse;
readonly decodeBlockResults: (response: JsonRpcSuccessResponse) => responses.BlockResultsResponse;
readonly decodeBlockSearch: (response: JsonRpcSuccessResponse) => responses.BlockSearchResponse;
readonly decodeBlockchain: (response: JsonRpcSuccessResponse) => responses.BlockchainResponse;
readonly decodeBroadcastTxSync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxSyncResponse;
readonly decodeBroadcastTxAsync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxAsyncResponse;

View File

@ -30,6 +30,21 @@ function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams):
};
}
interface RpcBlockSearchParams {
readonly query: string;
readonly page?: string;
readonly per_page?: string;
readonly order_by?: string;
}
function encodeBlockSearchParams(params: requests.BlockSearchParams): RpcBlockSearchParams {
return {
query: params.query,
page: may(Integer.encode, params.page),
per_page: may(Integer.encode, params.per_page),
order_by: params.order_by,
};
}
interface RpcAbciQueryParams {
readonly path: string;
/** hex encoded */
@ -118,6 +133,10 @@ export class Params {
return createJsonRpcRequest(req.method, encodeHeightParam(req.params));
}
public static encodeBlockSearch(req: requests.BlockSearchRequest): JsonRpcRequest {
return createJsonRpcRequest(req.method, encodeBlockSearchParams(req.params));
}
public static encodeBroadcastTx(req: requests.BroadcastTxRequest): JsonRpcRequest {
return createJsonRpcRequest(req.method, encodeBroadcastTxParams(req.params));
}

View File

@ -783,6 +783,18 @@ function decodeBlockResponse(data: RpcBlockResponse): responses.BlockResponse {
};
}
interface RpcBlockSearchResponse {
readonly blocks: readonly RpcBlockResponse[];
readonly total_count: string;
}
function decodeBlockSearch(data: RpcBlockSearchResponse): responses.BlockSearchResponse {
return {
totalCount: Integer.parse(assertNotEmpty(data.total_count)),
blocks: assertArray(data.blocks).map(decodeBlockResponse),
};
}
export class Responses {
public static decodeAbciInfo(response: JsonRpcSuccessResponse): responses.AbciInfoResponse {
return decodeAbciInfo(assertObject((response.result as AbciInfoResult).response));
@ -800,6 +812,10 @@ export class Responses {
return decodeBlockResults(response.result as RpcBlockResultsResponse);
}
public static decodeBlockSearch(response: JsonRpcSuccessResponse): responses.BlockSearchResponse {
return decodeBlockSearch(response.result as RpcBlockSearchResponse);
}
public static decodeBlockchain(response: JsonRpcSuccessResponse): responses.BlockchainResponse {
return decodeBlockchain(response.result as RpcBlockchainResponse);
}

View File

@ -8,6 +8,8 @@ export {
AbciQueryRequest,
BlockRequest,
BlockchainRequest,
BlockSearchParams,
BlockSearchRequest,
BlockResultsRequest,
BroadcastTxRequest,
BroadcastTxParams,
@ -38,6 +40,7 @@ export {
BlockParams,
BlockResponse,
BlockResultsResponse,
BlockSearchResponse,
BroadcastTxAsyncResponse,
BroadcastTxCommitResponse,
broadcastTxCommitSuccess,

View File

@ -12,6 +12,7 @@ export enum Method {
/** Get block headers for minHeight <= height <= maxHeight. */
Blockchain = "blockchain",
BlockResults = "block_results",
BlockSearch = "block_search",
BroadcastTxAsync = "broadcast_tx_async",
BroadcastTxSync = "broadcast_tx_sync",
BroadcastTxCommit = "broadcast_tx_commit",
@ -30,6 +31,7 @@ export type Request =
| AbciInfoRequest
| AbciQueryRequest
| BlockRequest
| BlockSearchRequest
| BlockchainRequest
| BlockResultsRequest
| BroadcastTxRequest
@ -97,6 +99,18 @@ export interface BlockResultsRequest {
};
}
export interface BlockSearchRequest {
readonly method: Method.BlockSearch;
readonly params: BlockSearchParams;
}
export interface BlockSearchParams {
readonly query: string;
readonly page?: number;
readonly per_page?: number;
readonly order_by?: string;
}
export interface BroadcastTxRequest {
readonly method: Method.BroadcastTxAsync | Method.BroadcastTxSync | Method.BroadcastTxCommit;
readonly params: BroadcastTxParams;

View File

@ -60,6 +60,11 @@ export interface BlockResultsResponse {
readonly endBlockEvents: readonly Event[];
}
export interface BlockSearchResponse {
readonly blocks: readonly BlockResponse[];
readonly totalCount: number;
}
export interface BlockchainResponse {
readonly lastHeight: number;
readonly blockMetas: readonly BlockMeta[];

View File

@ -166,7 +166,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues)
const status = await client.status();
// node info
expect(status.nodeInfo.version).toEqual(expected.version);
expect(status.nodeInfo.version).toMatch(expected.version);
expect(status.nodeInfo.protocolVersion).toEqual({
p2p: expected.p2pVersion,
block: expected.blockVersion,
@ -204,6 +204,73 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues)
});
});
describe("blockSearch", () => {
beforeAll(async () => {
if (tendermintEnabled()) {
const client = await Tendermint34Client.create(rpcFactory());
// eslint-disable-next-line no-inner-declarations
async function sendTx(): Promise<void> {
const tx = buildKvTx(randomString(), randomString());
const txRes = await client.broadcastTxCommit({ tx: tx });
expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true);
expect(txRes.height).toBeTruthy();
expect(txRes.hash.length).not.toEqual(0);
}
// send 3 txs
await sendTx();
await sendTx();
await sendTx();
client.disconnect();
await tendermintSearchIndexUpdated();
}
});
it("can paginate over blockSearch results", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
// Note: Block height 1 is unsupported. This is fixed in https://github.com/cosmos/cosmjs/pull/815
// and will be released with CosmJS 0.26.
const query = buildQuery({ raw: "block.height >= 2 AND block.height <= 4" });
// expect one page of results
const s1 = await client.blockSearch({ query: query, page: 1, per_page: 2 });
expect(s1.totalCount).toEqual(3);
expect(s1.blocks.length).toEqual(2);
// second page
const s2 = await client.blockSearch({ query: query, page: 2, per_page: 2 });
expect(s2.totalCount).toEqual(3);
expect(s2.blocks.length).toEqual(1);
client.disconnect();
});
it("can get all search results in one call", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
// Note: Block height 1 is unsupported. This is fixed in https://github.com/cosmos/cosmjs/pull/815
// and will be released with CosmJS 0.26.
const query = buildQuery({ raw: "block.height >= 2 AND block.height <= 4" });
const sall = await client.blockSearchAll({ query: query, per_page: 2 });
expect(sall.totalCount).toEqual(3);
expect(sall.blocks.length).toEqual(3);
// make sure there are in order from lowest to highest height
const [b1, b2, b3] = sall.blocks;
expect(b2.block.header.height).toEqual(b1.block.header.height + 1);
expect(b3.block.header.height).toEqual(b2.block.header.height + 1);
client.disconnect();
});
});
describe("blockchain", () => {
it("returns latest in descending order by default", async () => {
pendingWithoutTendermint();

View File

@ -98,6 +98,53 @@ export class Tendermint34Client {
return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults);
}
/**
* Search for events that are in a block.
*
* NOTE
* This method will error on any node that is running a Tendermint version lower than 0.34.9.
*
* @see https://docs.tendermint.com/master/rpc/#/Info/block_search
*/
public async blockSearch(params: requests.BlockSearchParams): Promise<responses.BlockSearchResponse> {
const query: requests.BlockSearchRequest = { params: params, method: requests.Method.BlockSearch };
const resp = await this.doCall(query, this.p.encodeBlockSearch, this.r.decodeBlockSearch);
return {
...resp,
// make sure we sort by height, as tendermint may be sorting by string value of the height
blocks: [...resp.blocks].sort((a, b) => a.block.header.height - b.block.header.height),
};
}
// this should paginate through all blockSearch options to ensure it returns all results.
// starts with page 1 or whatever was provided (eg. to start on page 7)
//
// NOTE
// This method will error on any node that is running a Tendermint version lower than 0.34.9.
public async blockSearchAll(params: requests.BlockSearchParams): Promise<responses.BlockSearchResponse> {
let page = params.page || 1;
const blocks: responses.BlockResponse[] = [];
let done = false;
while (!done) {
const resp = await this.blockSearch({ ...params, page: page });
blocks.push(...resp.blocks);
if (blocks.length < resp.totalCount) {
page++;
} else {
done = true;
}
}
// make sure we sort by height, as tendermint may be sorting by string value of the height
// and the earlier items may be in a higher page than the later items
blocks.sort((a, b) => a.block.header.height - b.block.header.height);
return {
totalCount: blocks.length,
blocks: blocks,
};
}
/**
* Queries block headers filtered by minHeight <= height <= maxHeight.
*

View File

@ -1,9 +1,12 @@
import { toAscii } from "@cosmjs/encoding";
import { sleep } from "@cosmjs/utils";
export const chainIdMatcher = /^[-a-zA-Z0-9]{3,30}$/;
export const anyMatcher = /^.*$/; // Any string, including empty. Does not do more than a type check.
export interface ExpectedValues {
/** The Tendermint version as reported by Tendermint itself */
readonly version: string;
readonly version: string | RegExp;
readonly appCreator: string;
readonly p2pVersion: number;
readonly blockVersion: number;
@ -49,7 +52,7 @@ export const tendermintInstances: readonly TendermintInstance[] = [
version: "0.34.x",
blockTime: 500,
expected: {
version: "182fa32", // srsly?
version: anyMatcher,
appCreator: "Cosmoshi Netowoko",
p2pVersion: 8,
blockVersion: 11,
@ -60,8 +63,6 @@ export const tendermintInstances: readonly TendermintInstance[] = [
export const defaultInstance: TendermintInstance = tendermintInstances[0];
export const chainIdMatcher = /^[-a-zA-Z0-9]{3,30}$/;
export function tendermintEnabled(): boolean {
return !!process.env.TENDERMINT_ENABLED;
}

View File

@ -5,7 +5,7 @@ command -v shellcheck >/dev/null && shellcheck "$0"
# Find latest patch releases at https://hub.docker.com/r/tendermint/tendermint/tags/
declare -a TM_VERSIONS
TM_VERSIONS[33]=v0.33.8
TM_VERSIONS[34]=v0.34.0
TM_VERSIONS[34]=v0.34.10
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

View File

@ -4,7 +4,7 @@ command -v shellcheck >/dev/null && shellcheck "$0"
declare -a TM_VERSIONS
TM_VERSIONS[33]=v0.33.8
TM_VERSIONS[34]=v0.34.0
TM_VERSIONS[34]=v0.34.10
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"