diff --git a/src/lib/rpcClient.spec.ts b/src/lib/rpcClient.spec.ts index 469c505..a3f262b 100644 --- a/src/lib/rpcClient.spec.ts +++ b/src/lib/rpcClient.spec.ts @@ -209,4 +209,77 @@ describe('RPCClient', () => { // Verify the response is defined expect(result).toBeDefined() }) + + describe('blockResults validator updates', () => { + const blockResults = (validatorUpdates: unknown) => ({ + jsonrpc: '2.0', + id: 1, + result: { + height: '1', + txs_results: null, + finalize_block_events: [], + validator_updates: validatorUpdates, + consensus_param_updates: null, + }, + }) + + it('decodes the cometbft v1 flattened pub_key shape', async () => { + nock(mockRpcUris[0]) + .get('/block_results') + .query(true) + .reply( + 200, + blockResults([ + { + power: '3469906', + pub_key_bytes: 'fZqZqbnw0nQBHe76t9qIcvOyyHjoMH3upoHsUqRvcBU=', + pub_key_type: 'ed25519', + }, + ]) + ) + + const client = new RPCClient(mockRpcUris[0]) + const result = await client.blockResults(1) + + expect(result.validatorUpdates).toHaveLength(1) + expect(result.validatorUpdates[0].pubkey.algorithm).toBe('ed25519') + expect(result.validatorUpdates[0].votingPower).toBe(3469906n) + }) + + it('still decodes the legacy nested pub_key shape', async () => { + nock(mockRpcUris[0]) + .get('/block_results') + .query(true) + .reply( + 200, + blockResults([ + { + power: '100', + pub_key: { + type: 'tendermint/PubKeyEd25519', + value: 'fZqZqbnw0nQBHe76t9qIcvOyyHjoMH3upoHsUqRvcBU=', + }, + }, + ]) + ) + + const client = new RPCClient(mockRpcUris[0]) + const result = await client.blockResults(1) + + expect(result.validatorUpdates).toHaveLength(1) + expect(result.validatorUpdates[0].votingPower).toBe(100n) + }) + + it('handles blocks without validator updates', async () => { + nock(mockRpcUris[0]) + .get('/block_results') + .query(true) + .reply(200, blockResults(null)) + + const client = new RPCClient(mockRpcUris[0]) + const result = await client.blockResults(1) + + expect(result.validatorUpdates).toHaveLength(0) + }) + }) }) diff --git a/src/lib/rpcClient.ts b/src/lib/rpcClient.ts index b616f8e..84d5adc 100644 --- a/src/lib/rpcClient.ts +++ b/src/lib/rpcClient.ts @@ -243,9 +243,39 @@ function decodeBlockResults( rpcBlockResult.result.end_block_events = end_block_events } + rpcBlockResult.result.validator_updates = + rpcBlockResult.result.validator_updates?.map(normalizeValidatorUpdate) ?? + null + return Responses.decodeBlockResults(rpcBlockResult) } +const PUB_KEY_AMINO_TYPES: Record = { + ed25519: 'tendermint/PubKeyEd25519', + secp256k1: 'tendermint/PubKeySecp256k1', +} + +// cometbft v1 flattened `pub_key` into `pub_key_bytes`/`pub_key_type`, which +// the cosmjs decoder rejects. Blocks carrying a validator set change would +// otherwise stall the sync worker forever. +function normalizeValidatorUpdate( + update: RpcValidatorUpdate +): RpcValidatorUpdate { + if (update.pub_key !== undefined || update.pub_key_bytes === undefined) { + return update + } + + const type = PUB_KEY_AMINO_TYPES[update.pub_key_type ?? ''] + if (type === undefined) { + throw Error(`unknown pubkey type ${update.pub_key_type}`) + } + + return { + power: update.power, + pub_key: { type, value: update.pub_key_bytes }, + } +} + interface Header { header: { version: { @@ -287,6 +317,16 @@ interface RpcBlockResultsResponse { begin_block_events: RpcEvent[] | null end_block_events: RpcEvent[] | null finalize_block_events: RpcEvent[] | null + validator_updates: RpcValidatorUpdate[] | null +} + +interface RpcValidatorUpdate { + power?: string + /** pre-cometbft-v1 shape */ + pub_key?: { type: string; value: string } + /** cometbft v1 flattened shape */ + pub_key_bytes?: string + pub_key_type?: string } interface RpcTxData {