@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
148 lines • 6.4 kB
JavaScript
import EventEmitter from "node:events";
import { computeEpochAtSlot, computeTimeAtSlot, getCurrentSlot } from "@lodestar/state-transition";
import { ErrorAborted } from "@lodestar/utils";
export { ClockEvent };
var ClockEvent;
(function (ClockEvent) {
/**
* This event signals the start of a new slot, and that subsequent calls to `clock.currentSlot` will equal `slot`.
* This event is guaranteed to be emitted every `SLOT_DURATION_MS` milliseconds.
*/
ClockEvent["slot"] = "clock:slot";
/**
* This event signals the start of a new epoch, and that subsequent calls to `clock.currentEpoch` will return `epoch`.
* This event is guaranteed to be emitted every `SLOT_DURATION_MS * SLOTS_PER_EPOCH` milliseconds.
*/
ClockEvent["epoch"] = "clock:epoch";
})(ClockEvent || (ClockEvent = {}));
/**
* A local clock, the clock time is assumed to be trusted
*/
export class Clock extends EventEmitter {
genesisTime;
config;
timeoutId;
signal;
_currentSlot;
constructor({ config, genesisTime, signal }) {
super();
this.config = config;
this.genesisTime = genesisTime;
this.timeoutId = setTimeout(this.onNextSlot, this.msUntilNextSlot());
this.signal = signal;
this._currentSlot = getCurrentSlot(this.config, this.genesisTime);
this.signal.addEventListener("abort", () => clearTimeout(this.timeoutId), { once: true });
}
get currentSlot() {
const slot = getCurrentSlot(this.config, this.genesisTime);
if (slot > this._currentSlot) {
clearTimeout(this.timeoutId);
this.onNextSlot(slot);
}
return slot;
}
/**
* If it's too close to next slot given MAXIMUM_GOSSIP_CLOCK_DISPARITY, return currentSlot + 1.
* Otherwise return currentSlot
*
* Spec: phase0/p2p-interface.md - gossip validation uses `current_time + MAXIMUM_GOSSIP_CLOCK_DISPARITY < message_time`
* to reject future messages (strict `<`), so the boundary (exactly equal) is accepted, hence `<=` here.
*/
get currentSlotWithGossipDisparity() {
const currentSlot = this.currentSlot;
const nextSlotTime = computeTimeAtSlot(this.config, currentSlot + 1, this.genesisTime) * 1000;
return nextSlotTime - Date.now() <= this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY ? currentSlot + 1 : currentSlot;
}
get currentEpoch() {
return computeEpochAtSlot(this.currentSlot);
}
/** Returns the slot if the internal clock were advanced by `toleranceSec`. */
slotWithFutureTolerance(toleranceSec) {
// this is the same to getting slot at now + toleranceSec
return getCurrentSlot(this.config, this.genesisTime - toleranceSec);
}
/** Returns the slot if the internal clock were reversed by `toleranceSec`. */
slotWithPastTolerance(toleranceSec) {
// this is the same to getting slot at now - toleranceSec
return getCurrentSlot(this.config, this.genesisTime + toleranceSec);
}
/**
* Check if a slot is current slot given MAXIMUM_GOSSIP_CLOCK_DISPARITY.
*
* Uses `<=` for disparity checks because the spec rejects with strict `<`
* (phase0/p2p-interface.md), meaning the boundary (exactly equal) is accepted.
*/
isCurrentSlotGivenGossipDisparity(slot) {
const currentSlot = this.currentSlot;
if (currentSlot === slot) {
return true;
}
const nextSlotTime = computeTimeAtSlot(this.config, currentSlot + 1, this.genesisTime) * 1000;
// we're too close to next slot, accept next slot
if (nextSlotTime - Date.now() <= this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY) {
return slot === currentSlot + 1;
}
const currentSlotTime = computeTimeAtSlot(this.config, currentSlot, this.genesisTime) * 1000;
// we've just passed the current slot, accept previous slot
if (Date.now() - currentSlotTime <= this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY) {
return slot === currentSlot - 1;
}
return false;
}
async waitForSlot(slot) {
if (this.signal.aborted) {
throw new ErrorAborted();
}
if (this.currentSlot >= slot) {
return;
}
return new Promise((resolve, reject) => {
const onSlot = (clockSlot) => {
if (clockSlot >= slot) {
onDone();
}
};
const onDone = () => {
this.off(ClockEvent.slot, onSlot);
this.signal.removeEventListener("abort", onAbort);
resolve();
};
const onAbort = () => {
this.off(ClockEvent.slot, onSlot);
reject(new ErrorAborted());
};
this.on(ClockEvent.slot, onSlot);
this.signal.addEventListener("abort", onAbort, { once: true });
});
}
secFromSlot(slot, toSec = Date.now() / 1000) {
return toSec - computeTimeAtSlot(this.config, slot, this.genesisTime);
}
msFromSlot(slot, toMs = Date.now()) {
return toMs - computeTimeAtSlot(this.config, slot, this.genesisTime) * 1000;
}
onNextSlot = (slot) => {
const clockSlot = slot ?? getCurrentSlot(this.config, this.genesisTime);
// process multiple clock slots in the case the main thread has been saturated for > SLOT_DURATION_MS
while (this._currentSlot < clockSlot && !this.signal.aborted) {
const previousSlot = this._currentSlot;
this._currentSlot++;
this.emit(ClockEvent.slot, this._currentSlot);
const previousEpoch = computeEpochAtSlot(previousSlot);
const currentEpoch = computeEpochAtSlot(this._currentSlot);
if (previousEpoch < currentEpoch) {
this.emit(ClockEvent.epoch, currentEpoch);
}
}
if (!this.signal.aborted) {
//recursively invoke onNextSlot
this.timeoutId = setTimeout(this.onNextSlot, this.msUntilNextSlot());
}
};
msUntilNextSlot() {
const milliSecondsPerSlot = this.config.SLOT_DURATION_MS;
const diffInMilliSeconds = Date.now() - this.genesisTime * 1000;
return milliSecondsPerSlot - (diffInMilliSeconds % milliSecondsPerSlot);
}
}
//# sourceMappingURL=clock.js.map