Skip to content

Commit 9767c22

Browse files
authored
Merge pull request #815 from cosmos/add-block-search-simon
Add support for /block_search RPC endpoint in Tendermint 0.34.9+
2 parents 02f5439 + 8d3c93a commit 9767c22

File tree

11 files changed

+232
-17
lines changed

11 files changed

+232
-17
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ and this project adheres to
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- @cosmjs/tendermint-rpc: `Tendermint34Client.blockSearch` and
12+
`Tendermint34Client.blockSearchAll` were added to allow searching blocks in
13+
Tendermint 0.34.9+ backends.
14+
15+
### Changes
16+
17+
- @cosmjs/tendermint-rpc: Make `tendermint34.Header.lastBlockId` and
18+
`tendermint34.Block.lastCommit` optional to better handle the case of height 1
19+
where there is no previous block.
20+
921
## [0.25.3] - 2021-05-18
1022

1123
### Fixed

packages/stargate/src/queries/queryclient.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
import { iavlSpec, ics23, tendermintSpec, verifyExistence, verifyNonExistence } from "@confio/ics23";
33
import { toAscii, toHex } from "@cosmjs/encoding";
44
import { firstEvent } from "@cosmjs/stream";
5-
import { Header, NewBlockHeaderEvent, ProofOp, Tendermint34Client } from "@cosmjs/tendermint-rpc";
5+
import { tendermint34, Tendermint34Client } from "@cosmjs/tendermint-rpc";
66
import { arrayContentEquals, assert, assertDefined, isNonNullObject, sleep } from "@cosmjs/utils";
77
import { Stream } from "xstream";
88

99
import { ProofOps } from "../codec/tendermint/crypto/proof";
1010

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

13-
function checkAndParseOp(op: ProofOp, kind: string, key: Uint8Array): ics23.CommitmentProof {
13+
function checkAndParseOp(op: tendermint34.ProofOp, kind: string, key: Uint8Array): ics23.CommitmentProof {
1414
if (op.type !== kind) {
1515
throw new Error(`Op expected to be ${kind}, got "${op.type}`);
1616
}
@@ -587,15 +587,15 @@ export class QueryClient {
587587

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

596596
const searchHeight = height + 1;
597-
let nextHeader: Header | undefined;
598-
let headersSubscription: Stream<NewBlockHeaderEvent> | undefined;
597+
let nextHeader: tendermint34.Header | undefined;
598+
let headersSubscription: Stream<tendermint34.NewBlockHeaderEvent> | undefined;
599599
try {
600600
headersSubscription = this.tmClient.subscribeNewBlockHeader();
601601
} catch {

packages/tendermint-rpc/src/tendermint34/adaptor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface Params {
2323
readonly encodeBlock: (req: requests.BlockRequest) => JsonRpcRequest;
2424
readonly encodeBlockchain: (req: requests.BlockchainRequest) => JsonRpcRequest;
2525
readonly encodeBlockResults: (req: requests.BlockResultsRequest) => JsonRpcRequest;
26+
readonly encodeBlockSearch: (req: requests.BlockSearchRequest) => JsonRpcRequest;
2627
readonly encodeBroadcastTx: (req: requests.BroadcastTxRequest) => JsonRpcRequest;
2728
readonly encodeCommit: (req: requests.CommitRequest) => JsonRpcRequest;
2829
readonly encodeGenesis: (req: requests.GenesisRequest) => JsonRpcRequest;
@@ -39,6 +40,7 @@ export interface Responses {
3940
readonly decodeAbciQuery: (response: JsonRpcSuccessResponse) => responses.AbciQueryResponse;
4041
readonly decodeBlock: (response: JsonRpcSuccessResponse) => responses.BlockResponse;
4142
readonly decodeBlockResults: (response: JsonRpcSuccessResponse) => responses.BlockResultsResponse;
43+
readonly decodeBlockSearch: (response: JsonRpcSuccessResponse) => responses.BlockSearchResponse;
4244
readonly decodeBlockchain: (response: JsonRpcSuccessResponse) => responses.BlockchainResponse;
4345
readonly decodeBroadcastTxSync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxSyncResponse;
4446
readonly decodeBroadcastTxAsync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxAsyncResponse;

packages/tendermint-rpc/src/tendermint34/adaptors/v0-34/requests.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams):
3030
};
3131
}
3232

33+
interface RpcBlockSearchParams {
34+
readonly query: string;
35+
readonly page?: string;
36+
readonly per_page?: string;
37+
readonly order_by?: string;
38+
}
39+
function encodeBlockSearchParams(params: requests.BlockSearchParams): RpcBlockSearchParams {
40+
return {
41+
query: params.query,
42+
page: may(Integer.encode, params.page),
43+
per_page: may(Integer.encode, params.per_page),
44+
order_by: params.order_by,
45+
};
46+
}
47+
3348
interface RpcAbciQueryParams {
3449
readonly path: string;
3550
/** hex encoded */
@@ -118,6 +133,10 @@ export class Params {
118133
return createJsonRpcRequest(req.method, encodeHeightParam(req.params));
119134
}
120135

136+
public static encodeBlockSearch(req: requests.BlockSearchRequest): JsonRpcRequest {
137+
return createJsonRpcRequest(req.method, encodeBlockSearchParams(req.params));
138+
}
139+
121140
public static encodeBroadcastTx(req: requests.BroadcastTxRequest): JsonRpcRequest {
122141
return createJsonRpcRequest(req.method, encodeBroadcastTxParams(req.params));
123142
}

packages/tendermint-rpc/src/tendermint34/adaptors/v0-34/responses.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -341,15 +341,17 @@ function decodeHeader(data: RpcHeader): responses.Header {
341341
height: Integer.parse(assertNotEmpty(data.height)),
342342
time: fromRfc3339WithNanoseconds(assertNotEmpty(data.time)),
343343

344-
lastBlockId: decodeBlockId(data.last_block_id),
344+
// When there is no last block ID (i.e. this block's height is 1), we get an empty structure like this:
345+
// { hash: '', parts: { total: 0, hash: '' } }
346+
lastBlockId: data.last_block_id.hash ? decodeBlockId(data.last_block_id) : null,
345347

346-
lastCommitHash: fromHex(assertNotEmpty(data.last_commit_hash)),
348+
lastCommitHash: fromHex(assertSet(data.last_commit_hash)),
347349
dataHash: fromHex(assertSet(data.data_hash)),
348350

349-
validatorsHash: fromHex(assertNotEmpty(data.validators_hash)),
350-
nextValidatorsHash: fromHex(assertNotEmpty(data.next_validators_hash)),
351-
consensusHash: fromHex(assertNotEmpty(data.consensus_hash)),
352-
appHash: fromHex(assertNotEmpty(data.app_hash)),
351+
validatorsHash: fromHex(assertSet(data.validators_hash)),
352+
nextValidatorsHash: fromHex(assertSet(data.next_validators_hash)),
353+
consensusHash: fromHex(assertSet(data.consensus_hash)),
354+
appHash: fromHex(assertSet(data.app_hash)),
353355
lastResultsHash: fromHex(assertSet(data.last_results_hash)),
354356

355357
evidenceHash: fromHex(assertSet(data.evidence_hash)),
@@ -765,7 +767,9 @@ interface RpcBlock {
765767
function decodeBlock(data: RpcBlock): responses.Block {
766768
return {
767769
header: decodeHeader(assertObject(data.header)),
768-
lastCommit: decodeCommit(assertObject(data.last_commit)),
770+
// For the block at height 1, last commit is not set. This is represented in an empty object like this:
771+
// { height: '0', round: 0, block_id: { hash: '', parts: [Object] }, signatures: [] }
772+
lastCommit: data.last_commit.block_id.hash ? decodeCommit(assertObject(data.last_commit)) : null,
769773
txs: data.data.txs ? assertArray(data.data.txs).map(fromBase64) : [],
770774
evidence: data.evidence && may(decodeEvidences, data.evidence.evidence),
771775
};
@@ -783,6 +787,18 @@ function decodeBlockResponse(data: RpcBlockResponse): responses.BlockResponse {
783787
};
784788
}
785789

790+
interface RpcBlockSearchResponse {
791+
readonly blocks: readonly RpcBlockResponse[];
792+
readonly total_count: string;
793+
}
794+
795+
function decodeBlockSearch(data: RpcBlockSearchResponse): responses.BlockSearchResponse {
796+
return {
797+
totalCount: Integer.parse(assertNotEmpty(data.total_count)),
798+
blocks: assertArray(data.blocks).map(decodeBlockResponse),
799+
};
800+
}
801+
786802
export class Responses {
787803
public static decodeAbciInfo(response: JsonRpcSuccessResponse): responses.AbciInfoResponse {
788804
return decodeAbciInfo(assertObject((response.result as AbciInfoResult).response));
@@ -800,6 +816,10 @@ export class Responses {
800816
return decodeBlockResults(response.result as RpcBlockResultsResponse);
801817
}
802818

819+
public static decodeBlockSearch(response: JsonRpcSuccessResponse): responses.BlockSearchResponse {
820+
return decodeBlockSearch(response.result as RpcBlockSearchResponse);
821+
}
822+
803823
public static decodeBlockchain(response: JsonRpcSuccessResponse): responses.BlockchainResponse {
804824
return decodeBlockchain(response.result as RpcBlockchainResponse);
805825
}

packages/tendermint-rpc/src/tendermint34/hasher.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ function hashTree(hashes: readonly Uint8Array[]): Uint8Array {
4646
}
4747

4848
export function hashBlock(header: Header): Uint8Array {
49+
if (!header.lastBlockId) {
50+
throw new Error(
51+
"Hashing a block header with no last block ID (i.e. header at height 1) is not supported. If you need this, contributions are welcome. Please add documentation and test vectors for this case.",
52+
);
53+
}
54+
4955
const encodedFields: readonly Uint8Array[] = [
5056
encodeVersion(header.version),
5157
encodeString(header.chainId),

packages/tendermint-rpc/src/tendermint34/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export {
88
AbciQueryRequest,
99
BlockRequest,
1010
BlockchainRequest,
11+
BlockSearchParams,
12+
BlockSearchRequest,
1113
BlockResultsRequest,
1214
BroadcastTxRequest,
1315
BroadcastTxParams,
@@ -38,6 +40,7 @@ export {
3840
BlockParams,
3941
BlockResponse,
4042
BlockResultsResponse,
43+
BlockSearchResponse,
4144
BroadcastTxAsyncResponse,
4245
BroadcastTxCommitResponse,
4346
broadcastTxCommitSuccess,

packages/tendermint-rpc/src/tendermint34/requests.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum Method {
1212
/** Get block headers for minHeight <= height <= maxHeight. */
1313
Blockchain = "blockchain",
1414
BlockResults = "block_results",
15+
BlockSearch = "block_search",
1516
BroadcastTxAsync = "broadcast_tx_async",
1617
BroadcastTxSync = "broadcast_tx_sync",
1718
BroadcastTxCommit = "broadcast_tx_commit",
@@ -30,6 +31,7 @@ export type Request =
3031
| AbciInfoRequest
3132
| AbciQueryRequest
3233
| BlockRequest
34+
| BlockSearchRequest
3335
| BlockchainRequest
3436
| BlockResultsRequest
3537
| BroadcastTxRequest
@@ -60,6 +62,7 @@ export interface AbciQueryRequest {
6062
readonly method: Method.AbciQuery;
6163
readonly params: AbciQueryParams;
6264
}
65+
6366
export interface AbciQueryParams {
6467
readonly path: string;
6568
readonly data: Uint8Array;
@@ -97,10 +100,23 @@ export interface BlockResultsRequest {
97100
};
98101
}
99102

103+
export interface BlockSearchRequest {
104+
readonly method: Method.BlockSearch;
105+
readonly params: BlockSearchParams;
106+
}
107+
108+
export interface BlockSearchParams {
109+
readonly query: string;
110+
readonly page?: number;
111+
readonly per_page?: number;
112+
readonly order_by?: string;
113+
}
114+
100115
export interface BroadcastTxRequest {
101116
readonly method: Method.BroadcastTxAsync | Method.BroadcastTxSync | Method.BroadcastTxCommit;
102117
readonly params: BroadcastTxParams;
103118
}
119+
104120
export interface BroadcastTxParams {
105121
readonly tx: Uint8Array;
106122
}
@@ -141,6 +157,7 @@ export interface TxRequest {
141157
readonly method: Method.Tx;
142158
readonly params: TxParams;
143159
}
160+
144161
export interface TxParams {
145162
readonly hash: Uint8Array;
146163
readonly prove?: boolean;

packages/tendermint-rpc/src/tendermint34/responses.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ export interface BlockResultsResponse {
6060
readonly endBlockEvents: readonly Event[];
6161
}
6262

63+
export interface BlockSearchResponse {
64+
readonly blocks: readonly BlockResponse[];
65+
readonly totalCount: number;
66+
}
67+
6368
export interface BlockchainResponse {
6469
readonly lastHeight: number;
6570
readonly blockMetas: readonly BlockMeta[];
@@ -212,7 +217,10 @@ export interface BlockId {
212217

213218
export interface Block {
214219
readonly header: Header;
215-
readonly lastCommit: Commit;
220+
/**
221+
* For the block at height 1, last commit is not set.
222+
*/
223+
readonly lastCommit: Commit | null;
216224
readonly txs: readonly Uint8Array[];
217225
readonly evidence?: readonly Evidence[];
218226
}
@@ -264,21 +272,39 @@ export interface Header {
264272
readonly height: number;
265273
readonly time: ReadonlyDateWithNanoseconds;
266274

267-
// prev block info
268-
readonly lastBlockId: BlockId;
275+
/**
276+
* Block ID of the previous block. This can be `null` when the currect block is height 1.
277+
*/
278+
readonly lastBlockId: BlockId | null;
269279

270-
// hashes of block data
280+
/**
281+
* Hashes of block data.
282+
*
283+
* This is `sha256("")` for height 1 🤷‍
284+
*/
271285
readonly lastCommitHash: Uint8Array;
272-
readonly dataHash: Uint8Array; // empty when number of transaction is 0
286+
/**
287+
* This is `sha256("")` as long as there is no data 🤷‍
288+
*/
289+
readonly dataHash: Uint8Array;
273290

274291
// hashes from the app output from the prev block
275292
readonly validatorsHash: Uint8Array;
276293
readonly nextValidatorsHash: Uint8Array;
277294
readonly consensusHash: Uint8Array;
295+
/**
296+
* This can be an empty string for height 1 and turn into "0000000000000000" later on 🤷‍
297+
*/
278298
readonly appHash: Uint8Array;
299+
/**
300+
* This is `sha256("")` as long as there is no data 🤷‍
301+
*/
279302
readonly lastResultsHash: Uint8Array;
280303

281304
// consensus info
305+
/**
306+
* This is `sha256("")` as long as there is no data 🤷‍
307+
*/
282308
readonly evidenceHash: Uint8Array;
283309
readonly proposerAddress: Uint8Array;
284310
}

packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,69 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues)
204204
});
205205
});
206206

207+
describe("blockSearch", () => {
208+
beforeAll(async () => {
209+
if (tendermintEnabled()) {
210+
const client = await Tendermint34Client.create(rpcFactory());
211+
212+
// eslint-disable-next-line no-inner-declarations
213+
async function sendTx(): Promise<void> {
214+
const tx = buildKvTx(randomString(), randomString());
215+
216+
const txRes = await client.broadcastTxCommit({ tx: tx });
217+
expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true);
218+
expect(txRes.height).toBeTruthy();
219+
expect(txRes.hash.length).not.toEqual(0);
220+
}
221+
222+
// send 3 txs
223+
await sendTx();
224+
await sendTx();
225+
await sendTx();
226+
227+
client.disconnect();
228+
229+
await tendermintSearchIndexUpdated();
230+
}
231+
});
232+
233+
it("can paginate over blockSearch results", async () => {
234+
pendingWithoutTendermint();
235+
const client = await Tendermint34Client.create(rpcFactory());
236+
237+
const query = buildQuery({ raw: "block.height >= 1 AND block.height <= 3" });
238+
239+
// expect one page of results
240+
const s1 = await client.blockSearch({ query: query, page: 1, per_page: 2 });
241+
expect(s1.totalCount).toEqual(3);
242+
expect(s1.blocks.length).toEqual(2);
243+
244+
// second page
245+
const s2 = await client.blockSearch({ query: query, page: 2, per_page: 2 });
246+
expect(s2.totalCount).toEqual(3);
247+
expect(s2.blocks.length).toEqual(1);
248+
249+
client.disconnect();
250+
});
251+
252+
it("can get all search results in one call", async () => {
253+
pendingWithoutTendermint();
254+
const client = await Tendermint34Client.create(rpcFactory());
255+
256+
const query = buildQuery({ raw: "block.height >= 1 AND block.height <= 3" });
257+
258+
const sall = await client.blockSearchAll({ query: query, per_page: 2 });
259+
expect(sall.totalCount).toEqual(3);
260+
expect(sall.blocks.length).toEqual(3);
261+
// make sure there are in order from lowest to highest height
262+
const [b1, b2, b3] = sall.blocks;
263+
expect(b2.block.header.height).toEqual(b1.block.header.height + 1);
264+
expect(b3.block.header.height).toEqual(b2.block.header.height + 1);
265+
266+
client.disconnect();
267+
});
268+
});
269+
207270
describe("blockchain", () => {
208271
it("returns latest in descending order by default", async () => {
209272
pendingWithoutTendermint();

0 commit comments

Comments
 (0)