From d7186e086fff69cd6714916229d740af3ba4004c Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 7 Aug 2023 11:04:09 -0600 Subject: [PATCH 1/2] refactor(experimental): add getLeaderSchedule API method --- .../__tests__/get-leader-schedule-test.ts | 110 ++++++++++++++++++ .../src/rpc-methods/getLeaderSchedule.ts | 41 +++++++ packages/rpc-core/src/rpc-methods/index.ts | 2 + packages/rpc-core/tsconfig.json | 3 +- 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts create mode 100644 packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts diff --git a/packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts b/packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts new file mode 100644 index 000000000000..89f70c807046 --- /dev/null +++ b/packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts @@ -0,0 +1,110 @@ +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, Slot } 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; + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.dontMock(); + rpc = createJsonRpc({ + api: createSolanaRpcApi(), + transport: createHttpTransport({ url: 'http://127.0.0.1:8899' }), + }); + }); + + (['confirmed', 'finalized', 'processed'] as Commitment[]).forEach(commitment => { + ([undefined, 0n] as (Slot | undefined)[]).forEach(slot => { + describe(`given a ${commitment} commitment and ${slot} slot`, () => { + // TODO: With added control over the local validator, we + // should add a check to ensure these are actually the right epoch + const slotDescription = slot ? `corresponding` : 'current'; + describe('given no identity', () => { + it(`returns the leader schedule for all cluster nodes in the ${slotDescription} epoch`, async () => { + expect.assertions(3); + const res = await rpc.getLeaderSchedule(slot, { commitment }).send(); + 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('given an account that is a validator identity', () => { + it(`returns the leader schedule for only the specified node in the ${slotDescription} epoch`, async () => { + expect.assertions(1); + const identity = getValidatorAddress(); + const res = await rpc + .getLeaderSchedule(slot, { + commitment, + identity, + }) + .send(); + 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(undefined, { + 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(undefined, { + 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(); + }); + }); + }); +}); diff --git a/packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts b/packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts new file mode 100644 index 000000000000..46554382077c --- /dev/null +++ b/packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts @@ -0,0 +1,41 @@ +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 GetLeaderScheduleApiResponse = Readonly<{ + [key: Base58EncodedAddress]: Slot[]; +}> | null; + +export interface GetLeaderScheduleApi { + /** + * Returns the current block height of the node + */ + getLeaderSchedule( + /** + * Fetch the leader schedule for the epoch that corresponds to the provided slot. + * If unspecified, the leader schedule for the current epoch is fetched + */ + slot?: Slot, + config?: Readonly<{ + commitment?: Commitment; + /** Only return results for this validator identity (base58 encoded address) */ + identity?: Base58EncodedAddress; + }> + ): GetLeaderScheduleApiResponse; +} diff --git a/packages/rpc-core/src/rpc-methods/index.ts b/packages/rpc-core/src/rpc-methods/index.ts index 5dd3b757a67d..75c94e597d11 100644 --- a/packages/rpc-core/src/rpc-methods/index.ts +++ b/packages/rpc-core/src/rpc-methods/index.ts @@ -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'; @@ -57,6 +58,7 @@ export type SolanaRpcMethods = GetAccountInfoApi & GetHighestSnapshotSlotApi & GetInflationRewardApi & GetLatestBlockhashApi & + GetLeaderScheduleApi & GetMaxRetransmitSlotApi & GetMaxShredInsertSlotApi & GetRecentPerformanceSamplesApi & diff --git a/packages/rpc-core/tsconfig.json b/packages/rpc-core/tsconfig.json index f7a42ce4ebea..cc88735f00fe 100644 --- a/packages/rpc-core/tsconfig.json +++ b/packages/rpc-core/tsconfig.json @@ -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", From c281a64632828ddc2153e71e145d707c69202afc Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 7 Aug 2023 14:43:32 -0600 Subject: [PATCH 2/2] refactor(experimental): getSlotLeader overloading --- .../__tests__/get-leader-schedule-test.ts | 140 +++++++++++------- .../src/rpc-methods/getLeaderSchedule.ts | 26 ++-- 2 files changed, 102 insertions(+), 64 deletions(-) diff --git a/packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts b/packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts index 89f70c807046..5eabeb899844 100644 --- a/packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts +++ b/packages/rpc-core/src/rpc-methods/__tests__/get-leader-schedule-test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import fetchMock from 'jest-fetch-mock-fork'; import validatorIdentityBytes from '../../../../../test-ledger/validator-keypair.json'; -import { Commitment, Slot } from '../common'; +import { Commitment } from '../common'; import { createSolanaRpcApi, SolanaRpcMethods } from '../index'; function getValidatorAddress(): Base58EncodedAddress { @@ -28,72 +28,102 @@ describe('getLeaderSchedule', () => { }); (['confirmed', 'finalized', 'processed'] as Commitment[]).forEach(commitment => { - ([undefined, 0n] as (Slot | undefined)[]).forEach(slot => { - describe(`given a ${commitment} commitment and ${slot} slot`, () => { - // TODO: With added control over the local validator, we - // should add a check to ensure these are actually the right epoch - const slotDescription = slot ? `corresponding` : 'current'; - describe('given no identity', () => { - it(`returns the leader schedule for all cluster nodes in the ${slotDescription} epoch`, async () => { - expect.assertions(3); - const res = await rpc.getLeaderSchedule(slot, { commitment }).send(); - 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 \`${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('given an account that is a validator identity', () => { - it(`returns the leader schedule for only the specified node in the ${slotDescription} epoch`, async () => { - expect.assertions(1); - const identity = getValidatorAddress(); - const res = await rpc - .getLeaderSchedule(slot, { - commitment, - identity, - }) - .send(); - expect(res).toMatchObject({ - [identity]: 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('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(undefined, { - commitment, - // See scripts/fixtures/GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G.json - identity: 'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G' as Base58EncodedAddress, - }) - .send(); - expect(res).toMatchObject({}); + 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('given an account that does not exist', () => { - it('returns an empty object', async () => { - expect.assertions(1); - const res = await rpc - .getLeaderSchedule(undefined, { - commitment, - // Randomly generated - identity: 'BnWCFuxmi6uH3ceVx4R8qcbWBMPVVYVVFWtAiiTA1PAu' as Base58EncodedAddress, - }) - .send(); - expect(res).toMatchObject({}); + 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); diff --git a/packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts b/packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts index 46554382077c..3b33345b5079 100644 --- a/packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts +++ b/packages/rpc-core/src/rpc-methods/getLeaderSchedule.ts @@ -18,24 +18,32 @@ import { Commitment, Slot } from './common'; * } * ``` */ -type GetLeaderScheduleApiResponse = Readonly<{ +type GetLeaderScheduleApiResponseBase = Readonly<{ [key: Base58EncodedAddress]: Slot[]; -}> | null; +}>; export interface GetLeaderScheduleApi { /** - * Returns the current block height of the node + * 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( - /** - * Fetch the leader schedule for the epoch that corresponds to the provided slot. - * If unspecified, the leader schedule for the current epoch is fetched - */ - slot?: Slot, + slot: Slot, config?: Readonly<{ commitment?: Commitment; /** Only return results for this validator identity (base58 encoded address) */ identity?: Base58EncodedAddress; }> - ): GetLeaderScheduleApiResponse; + ): GetLeaderScheduleApiResponseBase | null; + getLeaderSchedule( + config?: Readonly<{ + commitment?: Commitment; + /** Only return results for this validator identity (base58 encoded address) */ + identity?: Base58EncodedAddress; + }> + ): GetLeaderScheduleApiResponseBase; }