feat: add convenience methods to EpochSchedule (#17810)
* first try, failing test * fix implementation and tests * lint:fix * move method tests to seperate test * lint fix * apply starry's comments and grab the bonus points * minor fixes after starry's second review Co-authored-by: Arrowana <8245419+Arrowana@users.noreply.github.com>
This commit is contained in:
parent
e0d679b319
commit
97ef9b2bc3
|
@ -27,6 +27,7 @@ import RpcClient from 'jayson/lib/client/browser';
|
||||||
import {IWSRequestParams} from 'rpc-websockets/dist/lib/client';
|
import {IWSRequestParams} from 'rpc-websockets/dist/lib/client';
|
||||||
|
|
||||||
import {AgentManager} from './agent-manager';
|
import {AgentManager} from './agent-manager';
|
||||||
|
import {EpochSchedule} from './epoch-schedule';
|
||||||
import {NonceAccount} from './nonce-account';
|
import {NonceAccount} from './nonce-account';
|
||||||
import {PublicKey} from './publickey';
|
import {PublicKey} from './publickey';
|
||||||
import {Signer} from './keypair';
|
import {Signer} from './keypair';
|
||||||
|
@ -373,23 +374,6 @@ const GetEpochInfoResult = pick({
|
||||||
transactionCount: optional(number()),
|
transactionCount: optional(number()),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Epoch schedule
|
|
||||||
* (see https://docs.solana.com/terminology#epoch)
|
|
||||||
*/
|
|
||||||
export type EpochSchedule = {
|
|
||||||
/** The maximum number of slots in each epoch */
|
|
||||||
slotsPerEpoch: number;
|
|
||||||
/** The number of slots before beginning of an epoch to calculate a leader schedule for that epoch */
|
|
||||||
leaderScheduleSlotOffset: number;
|
|
||||||
/** Indicates whether epochs start short and grow */
|
|
||||||
warmup: boolean;
|
|
||||||
/** The first epoch with `slotsPerEpoch` slots */
|
|
||||||
firstNormalEpoch: number;
|
|
||||||
/** The first slot of `firstNormalEpoch` */
|
|
||||||
firstNormalSlot: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const GetEpochScheduleResult = pick({
|
const GetEpochScheduleResult = pick({
|
||||||
slotsPerEpoch: number(),
|
slotsPerEpoch: number(),
|
||||||
leaderScheduleSlotOffset: number(),
|
leaderScheduleSlotOffset: number(),
|
||||||
|
@ -2788,7 +2772,14 @@ export class Connection {
|
||||||
if ('error' in res) {
|
if ('error' in res) {
|
||||||
throw new Error('failed to get epoch schedule: ' + res.error.message);
|
throw new Error('failed to get epoch schedule: ' + res.error.message);
|
||||||
}
|
}
|
||||||
return res.result;
|
const epochSchedule = res.result;
|
||||||
|
return new EpochSchedule(
|
||||||
|
epochSchedule.slotsPerEpoch,
|
||||||
|
epochSchedule.leaderScheduleSlotOffset,
|
||||||
|
epochSchedule.warmup,
|
||||||
|
epochSchedule.firstNormalEpoch,
|
||||||
|
epochSchedule.firstNormalSlot,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
const MINIMUM_SLOT_PER_EPOCH = 32;
|
||||||
|
|
||||||
|
// Returns the number of trailing zeros in the binary representation of self.
|
||||||
|
function trailingZeros(n: number) {
|
||||||
|
let trailingZeros = 0;
|
||||||
|
while (n > 1) {
|
||||||
|
n /= 2;
|
||||||
|
trailingZeros++;
|
||||||
|
}
|
||||||
|
return trailingZeros;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the smallest power of two greater than or equal to n
|
||||||
|
function nextPowerOfTwo(n: number) {
|
||||||
|
if (n === 0) return 1;
|
||||||
|
n--;
|
||||||
|
n |= n >> 1;
|
||||||
|
n |= n >> 2;
|
||||||
|
n |= n >> 4;
|
||||||
|
n |= n >> 8;
|
||||||
|
n |= n >> 16;
|
||||||
|
n |= n >> 32;
|
||||||
|
return n + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epoch schedule
|
||||||
|
* (see https://docs.solana.com/terminology#epoch)
|
||||||
|
* Can be retrieved with the {@link connection.getEpochSchedule} method
|
||||||
|
*/
|
||||||
|
export class EpochSchedule {
|
||||||
|
/** The maximum number of slots in each epoch */
|
||||||
|
public slotsPerEpoch: number;
|
||||||
|
/** The number of slots before beginning of an epoch to calculate a leader schedule for that epoch */
|
||||||
|
public leaderScheduleSlotOffset: number;
|
||||||
|
/** Indicates whether epochs start short and grow */
|
||||||
|
public warmup: boolean;
|
||||||
|
/** The first epoch with `slotsPerEpoch` slots */
|
||||||
|
public firstNormalEpoch: number;
|
||||||
|
/** The first slot of `firstNormalEpoch` */
|
||||||
|
public firstNormalSlot: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
slotsPerEpoch: number,
|
||||||
|
leaderScheduleSlotOffset: number,
|
||||||
|
warmup: boolean,
|
||||||
|
firstNormalEpoch: number,
|
||||||
|
firstNormalSlot: number,
|
||||||
|
) {
|
||||||
|
this.slotsPerEpoch = slotsPerEpoch;
|
||||||
|
this.leaderScheduleSlotOffset = leaderScheduleSlotOffset;
|
||||||
|
this.warmup = warmup;
|
||||||
|
this.firstNormalEpoch = firstNormalEpoch;
|
||||||
|
this.firstNormalSlot = firstNormalSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEpoch(slot: number): number {
|
||||||
|
return this.getEpochAndSlotIndex(slot)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getEpochAndSlotIndex(slot: number): [number, number] {
|
||||||
|
if (slot < this.firstNormalSlot) {
|
||||||
|
const epoch =
|
||||||
|
trailingZeros(nextPowerOfTwo(slot + MINIMUM_SLOT_PER_EPOCH + 1)) -
|
||||||
|
trailingZeros(MINIMUM_SLOT_PER_EPOCH) -
|
||||||
|
1;
|
||||||
|
|
||||||
|
const epochLen = this.getSlotsInEpoch(epoch);
|
||||||
|
const slotIndex = slot - (epochLen - MINIMUM_SLOT_PER_EPOCH);
|
||||||
|
return [epoch, slotIndex];
|
||||||
|
} else {
|
||||||
|
const normalSlotIndex = slot - this.firstNormalSlot;
|
||||||
|
const normalEpochIndex = Math.floor(normalSlotIndex / this.slotsPerEpoch);
|
||||||
|
const epoch = this.firstNormalEpoch + normalEpochIndex;
|
||||||
|
const slotIndex = normalSlotIndex % this.slotsPerEpoch;
|
||||||
|
return [epoch, slotIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstSlotInEpoch(epoch: number): number {
|
||||||
|
if (epoch <= this.firstNormalEpoch) {
|
||||||
|
return (Math.pow(2, epoch) - 1) * MINIMUM_SLOT_PER_EPOCH;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
(epoch - this.firstNormalEpoch) * this.slotsPerEpoch +
|
||||||
|
this.firstNormalSlot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastSlotInEpoch(epoch: number): number {
|
||||||
|
return this.getFirstSlotInEpoch(epoch) + this.getSlotsInEpoch(epoch) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSlotsInEpoch(epoch: number) {
|
||||||
|
if (epoch < this.firstNormalEpoch) {
|
||||||
|
return Math.pow(2, epoch + trailingZeros(MINIMUM_SLOT_PER_EPOCH));
|
||||||
|
} else {
|
||||||
|
return this.slotsPerEpoch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ export * from './blockhash';
|
||||||
export * from './bpf-loader-deprecated';
|
export * from './bpf-loader-deprecated';
|
||||||
export * from './bpf-loader';
|
export * from './bpf-loader';
|
||||||
export * from './connection';
|
export * from './connection';
|
||||||
|
export * from './epoch-schedule';
|
||||||
export * from './fee-calculator';
|
export * from './fee-calculator';
|
||||||
export * from './keypair';
|
export * from './keypair';
|
||||||
export * from './loader';
|
export * from './loader';
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Account,
|
Account,
|
||||||
Authorized,
|
Authorized,
|
||||||
Connection,
|
Connection,
|
||||||
|
EpochSchedule,
|
||||||
SystemProgram,
|
SystemProgram,
|
||||||
Transaction,
|
Transaction,
|
||||||
LAMPORTS_PER_SOL,
|
LAMPORTS_PER_SOL,
|
||||||
|
@ -24,7 +25,6 @@ import {
|
||||||
BLOCKHASH_CACHE_TIMEOUT_MS,
|
BLOCKHASH_CACHE_TIMEOUT_MS,
|
||||||
Commitment,
|
Commitment,
|
||||||
EpochInfo,
|
EpochInfo,
|
||||||
EpochSchedule,
|
|
||||||
InflationGovernor,
|
InflationGovernor,
|
||||||
SlotInfo,
|
SlotInfo,
|
||||||
} from '../src/connection';
|
} from '../src/connection';
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {expect} from 'chai';
|
||||||
|
|
||||||
|
import {EpochSchedule} from '../src';
|
||||||
|
|
||||||
|
describe('EpochSchedule', () => {
|
||||||
|
it('slot methods work', () => {
|
||||||
|
const firstNormalEpoch = 14;
|
||||||
|
const firstNormalSlot = 524_256;
|
||||||
|
const leaderScheduleSlotOffset = 432_000;
|
||||||
|
const slotsPerEpoch = 432_000;
|
||||||
|
const warmup = true;
|
||||||
|
|
||||||
|
const epochSchedule = new EpochSchedule(
|
||||||
|
slotsPerEpoch,
|
||||||
|
leaderScheduleSlotOffset,
|
||||||
|
warmup,
|
||||||
|
firstNormalEpoch,
|
||||||
|
firstNormalSlot,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(epochSchedule.getEpoch(35)).to.be.equal(1);
|
||||||
|
expect(epochSchedule.getEpochAndSlotIndex(35)).to.be.eql([1, 3]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
epochSchedule.getEpoch(firstNormalSlot + 3 * slotsPerEpoch + 12345),
|
||||||
|
).to.be.equal(17);
|
||||||
|
expect(
|
||||||
|
epochSchedule.getEpochAndSlotIndex(
|
||||||
|
firstNormalSlot + 3 * slotsPerEpoch + 12345,
|
||||||
|
),
|
||||||
|
).to.be.eql([17, 12345]);
|
||||||
|
|
||||||
|
expect(epochSchedule.getSlotsInEpoch(4)).to.be.equal(512);
|
||||||
|
expect(epochSchedule.getSlotsInEpoch(100)).to.be.equal(slotsPerEpoch);
|
||||||
|
|
||||||
|
expect(epochSchedule.getFirstSlotInEpoch(2)).to.be.equal(96);
|
||||||
|
expect(epochSchedule.getLastSlotInEpoch(2)).to.be.equal(223);
|
||||||
|
|
||||||
|
expect(epochSchedule.getFirstSlotInEpoch(16)).to.be.equal(
|
||||||
|
firstNormalSlot + 2 * slotsPerEpoch,
|
||||||
|
);
|
||||||
|
expect(epochSchedule.getLastSlotInEpoch(16)).to.be.equal(
|
||||||
|
firstNormalSlot + 3 * slotsPerEpoch - 1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue