Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(experimental): Add getBlockProduction API #1263

Merged
merged 3 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/rpc-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { IRpcApi, RpcRequest } from '@solana/rpc-transport/dist/types/json-rpc-t
import { patchParamsForSolanaLabsRpc } from './params-patcher';
import { patchResponseForSolanaLabsRpc } from './response-patcher';
import { GetAccountInfoApi } from './rpc-methods/getAccountInfo';
import { GetBalanceApi } from './rpc-methods/getBalance';
import { GetBlockHeightApi } from './rpc-methods/getBlockHeight';
import { GetBlockProductionApi } from './rpc-methods/getBlockProduction';
import { GetBlocksApi } from './rpc-methods/getBlocks';
import { GetInflationRewardApi } from './rpc-methods/getInflationReward';
import { GetBalanceApi } from './rpc-methods/getBalance';

type Config = Readonly<{
onIntegerOverflow?: (methodName: string, keyPath: (number | string)[], value: bigint) => void;
Expand All @@ -14,6 +15,7 @@ type Config = Readonly<{
export type SolanaRpcMethods = GetAccountInfoApi &
GetBalanceApi &
GetBlockHeightApi &
GetBlockProductionApi &
GetBlocksApi &
GetInflationRewardApi;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { createHttpTransport, createJsonRpc } from '@solana/rpc-transport';
import type { SolanaJsonRpcErrorCode } from '@solana/rpc-transport/dist/types/json-rpc-errors';
import type { Rpc } from '@solana/rpc-transport/dist/types/json-rpc-types';
import fetchMock from 'jest-fetch-mock-fork';
import { createSolanaRpcApi, SolanaRpcMethods } from '../../index';
import { Commitment } from '../common';
import { Base58EncodedAddress } from '@solana/keys';

describe('getBlockProduction', () => {
let rpc: Rpc<SolanaRpcMethods>;
beforeEach(() => {
fetchMock.resetMocks();
fetchMock.dontMock();
rpc = createJsonRpc<SolanaRpcMethods>({
api: createSolanaRpcApi(),
transport: createHttpTransport({ url: 'http://127.0.0.1:8899' }),
});
});

(['confirmed', 'finalized', 'processed'] as Commitment[]).forEach(commitment => {
describe(`when called with \`${commitment}\` commitment`, () => {
it('returns block production data', async () => {
expect.assertions(1);
const blockProductionPromise = rpc.getBlockProduction({ commitment }).send();
await expect(blockProductionPromise).resolves.toMatchObject({
value: expect.objectContaining({
byIdentity: expect.any(Object),
range: expect.objectContaining({
firstSlot: expect.any(BigInt),
lastSlot: expect.any(BigInt),
}),
}),
});
});

it('has the latest context slot as the last slot', async () => {
expect.assertions(1);
const blockProduction = await rpc.getBlockProduction({ commitment }).send();
expect(blockProduction.value.range.lastSlot).toBe(blockProduction.context.slot);
});
});
});

describe('when called with a single identity', () => {
// Currently this call always returns just one identity in tests, so no way to meaningfully test this
it.todo('returns data for just that identity');

it('returns an empty byIdentity if the identity is not a block producer', async () => {
expect.assertions(1);
// Randomly generated address, assumed not to be a block producer
const identity = '9NmqDDZa7mH1DBM4zeq9cm7VcRn2un1i2TwuMvjBoVhU' as Base58EncodedAddress;
const blockProductionPromise = rpc.getBlockProduction({ identity }).send();
await expect(blockProductionPromise).resolves.toMatchObject({
value: expect.objectContaining({
byIdentity: {},
}),
});
});
});

describe('when called with a `lastSlot` higher than the highest slot available', () => {
it('throws an error', async () => {
expect.assertions(1);
const blockProductionPromise = rpc
.getBlockProduction({
range: {
firstSlot: 0n,
lastSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason that this doesn't work is actually because the validator hasn't advanced to that slot yet. Apparently we code this as ‘invalid params (32602).’

Kind of sounds like we need an actual error code for this in the RPC, similar to JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed the 32602 error to JSON_RPC_INVALID_PARAMS for now, since that best matches the current behaviour. Would a github issue on the solana monorepo be the best place to track adding a custom error code?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Totally. That RPC method could do with throwing a more precise error.

},
})
.send();
await expect(blockProductionPromise).rejects.toMatchObject({
code: -32602 satisfies (typeof SolanaJsonRpcErrorCode)['JSON_RPC_INVALID_PARAMS'],
message: expect.any(String),
name: 'SolanaJsonRpcError',
});
});
});
});
46 changes: 46 additions & 0 deletions packages/rpc-core/src/rpc-methods/getBlockProduction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Base58EncodedAddress } from '@solana/keys';
import { Commitment, RpcResponse, U64UnsafeBeyond2Pow53Minus1 } from './common';

type NumberOfLeaderSlots = U64UnsafeBeyond2Pow53Minus1;
type NumberOfBlocksProduced = U64UnsafeBeyond2Pow53Minus1;

type SlotRange = Readonly<{
firstSlot: U64UnsafeBeyond2Pow53Minus1;
lastSlot: U64UnsafeBeyond2Pow53Minus1;
}>;

type GetBlockProductionApiConfigBase = Readonly<{
commitment?: Commitment;
range?: SlotRange;
}>;

type GetBlockProductionApiResponseBase = RpcResponse<{
range: SlotRange;
}>;

type GetBlockProductionApiResponseWithAllIdentities = Readonly<{
value: Readonly<{
byIdentity: Record<Base58EncodedAddress, [NumberOfLeaderSlots, NumberOfBlocksProduced]>;
}>;
}>;

type GetBlockProductionApiResponseWithSingleIdentity<TIdentity extends string> = Readonly<{
value: Readonly<{
byIdentity: Readonly<{ [TAddress in TIdentity]?: [NumberOfLeaderSlots, NumberOfBlocksProduced] }>;
}>;
}>;

export interface GetBlockProductionApi {
/**
* Returns recent block production information from the current or previous epoch.
*/
getBlockProduction<TIdentity extends Base58EncodedAddress>(
config: GetBlockProductionApiConfigBase &
Readonly<{
identity: TIdentity;
}>
): GetBlockProductionApiResponseBase & GetBlockProductionApiResponseWithSingleIdentity<TIdentity>;
getBlockProduction(
config?: GetBlockProductionApiConfigBase
): GetBlockProductionApiResponseBase & GetBlockProductionApiResponseWithAllIdentities;
}
1 change: 1 addition & 0 deletions packages/rpc-transport/src/json-rpc-errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Keep in sync with https://github.com/solana-labs/solana/blob/master/rpc-client-api/src/custom_error.rs
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
export const SolanaJsonRpcErrorCode = {
JSON_RPC_INVALID_PARAMS: -32602,
JSON_RPC_SCAN_ERROR: -32012,
JSON_RPC_SERVER_ERROR_BLOCK_CLEANED_UP: -32001,
JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE: -32004,
Expand Down