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 getLeaderSchedule API method #1494

Merged
merged 2 commits into from
Aug 8, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { base58 } from '@metaplex-foundation/umi-serializers';
import { Base58EncodedAddress } from '@solana/addresses';
import { createHttpTransport, createJsonRpc } from '@solana/rpc-transport';
import type { Rpc } from '@solana/rpc-transport/dist/types/json-rpc-types';
import assert from 'assert';
import fetchMock from 'jest-fetch-mock-fork';

import validatorIdentityBytes from '../../../../../test-ledger/validator-keypair.json';
import { Commitment } from '../common';
import { createSolanaRpcApi, SolanaRpcMethods } from '../index';

function getValidatorAddress(): Base58EncodedAddress {
const secretKey = new Uint8Array(validatorIdentityBytes);
const publicKey = secretKey.slice(32, 64);
const address = base58.deserialize(publicKey)[0];
return address as Base58EncodedAddress;
}

describe('getLeaderSchedule', () => {
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`, () => {
describe('when called with no identity and no slot', () => {
it('returns the leader schedule for all cluster nodes in the current epoch', async () => {
expect.assertions(3);
const res = await rpc.getLeaderSchedule({ commitment }).send();
// Does not need null check (default slot)
expect(res).toMatchObject(expect.any(Object));
for (const key of Object.keys(res)) {
expect(typeof key).toBe('string');
// Needs typecasting to be used as accessor
const base58Key: Base58EncodedAddress = key as Base58EncodedAddress;
expect(res[base58Key]).toMatchObject(expect.any(Array));
}
});
});

describe('when called with no identity and a valid slot', () => {
it('returns the leader schedule for all cluster nodes in the epoch corresponding to the provided slot', async () => {
expect.assertions(3);
const res = await rpc.getLeaderSchedule(0n, { commitment }).send();
// Needs null check (slot provided and may correspond to epoch that does not exist)
expect(res).toMatchObject(expect.any(Object));
assert(res);
for (const key of Object.keys(res)) {
expect(typeof key).toBe('string');
// Needs typecasting to be used as accessor
const base58Key: Base58EncodedAddress = key as Base58EncodedAddress;
expect(res[base58Key]).toMatchObject(expect.any(Array));
}
});
});

describe('when called with an account that is a validator identity and no slot', () => {
it('returns the leader schedule for only the specified node in the current epoch', async () => {
expect.assertions(1);
const identity = getValidatorAddress();
const res = await rpc
.getLeaderSchedule({
commitment,
identity,
})
.send();
// Does not need null check (default slot)
expect(res).toMatchObject({
[identity]: expect.any(Array),
});
});
});

describe('when called with an account that is a validator identity and a valid slot', () => {
it('returns the leader schedule for only the specified node in the epoch corresponding to the provided slot', async () => {
expect.assertions(1);
const identity = getValidatorAddress();
const res = await rpc
.getLeaderSchedule(0n, {
commitment,
identity,
})
.send();
// Needs null check (slot provided and may correspond to epoch that does not exist)
assert(res);
expect(res).toMatchObject({
[identity]: expect.any(Array),
});
});
});
});

describe('given an account that exists but is not a validator identity', () => {
it('returns an empty object', async () => {
expect.assertions(1);
const res = await rpc
.getLeaderSchedule({
commitment,
// See scripts/fixtures/GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G.json
identity: 'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G' as Base58EncodedAddress,
})
.send();
expect(res).toMatchObject({});
});
});

describe('given an account that does not exist', () => {
it('returns an empty object', async () => {
expect.assertions(1);
const res = await rpc
.getLeaderSchedule({
commitment,
// Randomly generated
identity: 'BnWCFuxmi6uH3ceVx4R8qcbWBMPVVYVVFWtAiiTA1PAu' as Base58EncodedAddress,
})
.send();
expect(res).toMatchObject({});
});
});

describe('given an invalid slot', () => {
it('returns an empty object', async () => {
expect.assertions(1);
const leaderSchedulePromise = rpc
.getLeaderSchedule(
2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high.
{ commitment }
)
.send();
await expect(leaderSchedulePromise).resolves.toBeNull();
});
});
});
});
49 changes: 49 additions & 0 deletions packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Base58EncodedAddress } from '@solana/addresses';

import { Commitment, Slot } from './common';

/**
* This return type is a dictionary of validator identities, as base-58 encoded
* strings, and their corresponding leader slot indices as values
* (indices are relative to the first slot in the requested epoch)
* @example
* ```json
* {
* "4Qkev8aNZcqFNSRhQzwyLMFSsi94jHqE8WNVTJzTP99F": [
* 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
* 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
* 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
* 57, 58, 59, 60, 61, 62, 63
* ]
* }
* ```
*/
type GetLeaderScheduleApiResponseBase = Readonly<{
[key: Base58EncodedAddress]: Slot[];
}>;

export interface GetLeaderScheduleApi {
/**
* Fetch the leader schedule for the epoch that corresponds to the provided slot.
* If unspecified, the leader schedule for the current epoch is fetched
*
* When a slot is provided, the leader schedule for the epoch that corresponds
* to the provided slot is returned, and this can be null if the slot corresponds
* to an epoch that does not exist
*/
getLeaderSchedule(
slot: Slot,
config?: Readonly<{
commitment?: Commitment;
/** Only return results for this validator identity (base58 encoded address) */
identity?: Base58EncodedAddress;
}>
): GetLeaderScheduleApiResponseBase | null;
getLeaderSchedule(
config?: Readonly<{
commitment?: Commitment;
/** Only return results for this validator identity (base58 encoded address) */
identity?: Base58EncodedAddress;
}>
): GetLeaderScheduleApiResponseBase;
}
2 changes: 2 additions & 0 deletions packages/rpc-core/src/rpc-methods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { GetHealthApi } from './getHealth';
import { GetHighestSnapshotSlotApi } from './getHighestSnapshotSlot';
import { GetInflationRewardApi } from './getInflationReward';
import { GetLatestBlockhashApi } from './getLatestBlockhash';
import { GetLeaderScheduleApi } from './getLeaderSchedule';
import { GetMaxRetransmitSlotApi } from './getMaxRetransmitSlot';
import { GetMaxShredInsertSlotApi } from './getMaxShredInsertSlot';
import { GetRecentPerformanceSamplesApi } from './getRecentPerformanceSamples';
Expand Down Expand Up @@ -57,6 +58,7 @@ export type SolanaRpcMethods = GetAccountInfoApi &
GetHighestSnapshotSlotApi &
GetInflationRewardApi &
GetLatestBlockhashApi &
GetLeaderScheduleApi &
GetMaxRetransmitSlotApi &
GetMaxShredInsertSlotApi &
GetRecentPerformanceSamplesApi &
Expand Down
3 changes: 2 additions & 1 deletion packages/rpc-core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"lib": ["DOM", "ES2017", "ES2020.BigInt", "ES2022.Error"]
"lib": ["DOM", "ES2017", "ES2020.BigInt", "ES2022.Error"],
"resolveJsonModule": true
},
"display": "@solana/rpc-core",
"extends": "tsconfig/base.json",
Expand Down