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: remove assumption of 3 intervals per slot #7067

Open
wants to merge 4 commits into
base: unstable
Choose a base branch
from
Open
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
5 changes: 4 additions & 1 deletion packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
PubkeyIndexMap,
EpochShuffling,
computeEndSlotAtEpoch,
getSlotFractionFromInterval,
SlotInterval,
computeAnchorCheckpoint,
} from "@lodestar/state-transition";
import {BeaconConfig} from "@lodestar/config";
Expand Down Expand Up @@ -226,7 +228,8 @@ export class BeaconChain implements IBeaconChain {

if (!clock) clock = new Clock({config, genesisTime: this.genesisTime, signal});

const preAggregateCutOffTime = (2 / 3) * this.config.SECONDS_PER_SLOT;
const preAggregateCutOffTime =
getSlotFractionFromInterval(SlotInterval.AGGREGATION_PROPAGATION) * this.config.SECONDS_PER_SLOT;
this.attestationPool = new AttestationPool(
config,
clock,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {phase0, Epoch, RootHex} from "@lodestar/types";
import {CachedBeaconStateAllForks, computeStartSlotAtEpoch, getBlockRootAtSlot} from "@lodestar/state-transition";
import {
CachedBeaconStateAllForks,
ONE_INTERVAL_OF_SLOT,
computeStartSlotAtEpoch,
getBlockRootAtSlot,
} from "@lodestar/state-transition";
import {Logger, MapDef, fromHex, sleep, toHex, toRootHex} from "@lodestar/utils";
import {routes} from "@lodestar/api";
import {loadCachedBeaconState} from "@lodestar/state-transition";
import {INTERVALS_PER_SLOT} from "@lodestar/params";
import {Metrics} from "../../metrics/index.js";
import {IClock} from "../../util/clock.js";
import {ShufflingCache} from "../shufflingCache.js";
Expand Down Expand Up @@ -476,14 +480,14 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
}

const blockSlot = state.slot;
const twoThirdsSlot = (2 * state.config.SECONDS_PER_SLOT) / INTERVALS_PER_SLOT;
const twoIntervalsFromSlot = 2 * ONE_INTERVAL_OF_SLOT * state.config.SECONDS_PER_SLOT;
// we always have clock in production, fallback value is only for test
const secFromSlot = this.clock?.secFromSlot(blockSlot) ?? twoThirdsSlot;
const secToTwoThirdsSlot = twoThirdsSlot - secFromSlot;
if (secToTwoThirdsSlot > 0) {
const secFromSlot = this.clock?.secFromSlot(blockSlot) ?? twoIntervalsFromSlot;
const secToTwoIntervalsFromSlot = twoIntervalsFromSlot - secFromSlot;
if (secToTwoIntervalsFromSlot > 0) {
// 2/3 of slot is the most free time of every slot, take that chance to persist checkpoint states
// normally it should only persist checkpoint states at 2/3 of slot 0 of epoch
await sleep(secToTwoThirdsSlot * 1000, this.signal);
await sleep(secToTwoIntervalsFromSlot * 1000, this.signal);
} else if (!this.processLateBlock) {
// normally the block persist happens at 2/3 of slot 0 of epoch, if it's already late then just skip to allow other tasks to run
// there are plenty of chances in the same epoch to persist checkpoint states, also if block is late it could be reorged
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function validateLightClientFinalityUpdate(
}

// [IGNORE] The finality_update is received after the block at signature_slot was given enough time to propagate
// through the network -- i.e. validate that one-third of finality_update.signature_slot has transpired
// through the network -- i.e. validate that first interval (1 / INTERVALS_PER_SLOT) of finality_update.signature_slot has transpired
// (SECONDS_PER_SLOT / INTERVALS_PER_SLOT seconds after the start of the slot, with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
if (updateReceivedTooEarly(config, chain.genesisTime, gossipedFinalityUpdate)) {
throw new LightClientError(GossipAction.IGNORE, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ChainForkConfig} from "@lodestar/config";
import {computeTimeAtSlot} from "@lodestar/state-transition";
import {computeTimeAtSlot, ONE_INTERVAL_OF_SLOT} from "@lodestar/state-transition";
import {LightClientOptimisticUpdate} from "@lodestar/types";
import {IBeaconChain} from "../interface.js";
import {LightClientError, LightClientErrorCode} from "../errors/lightClientError.js";
Expand All @@ -26,7 +26,7 @@ export function validateLightClientOptimisticUpdate(
}

// [IGNORE] The optimistic_update is received after the block at signature_slot was given enough time to propagate
// through the network -- i.e. validate that one-third of optimistic_update.signature_slot has transpired
// through the network -- i.e. validate that one interval of optimistic_update.signature_slot has transpired
// (SECONDS_PER_SLOT / INTERVALS_PER_SLOT seconds after the start of the slot, with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
if (updateReceivedTooEarly(config, chain.genesisTime, gossipedOptimisticUpdate)) {
throw new LightClientError(GossipAction.IGNORE, {
Expand All @@ -53,15 +53,16 @@ export function validateLightClientOptimisticUpdate(
* xxx|------- (x is not okay)
*
* [IGNORE] The *update is received after the block at signature_slot was given enough time to propagate
* through the network -- i.e. validate that one-third of *update.signature_slot has transpired
* through the network -- i.e. validate that one interval of *update.signature_slot has transpired
* (SECONDS_PER_SLOT / INTERVALS_PER_SLOT seconds after the start of the slot, with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
*/
export function updateReceivedTooEarly(
config: ChainForkConfig,
genesisTime: number,
update: Pick<LightClientOptimisticUpdate, "signatureSlot">
): boolean {
const signatureSlot13TimestampMs = computeTimeAtSlot(config, update.signatureSlot + 1 / 3, genesisTime) * 1000;
const signatureSlot13TimestampMs =
computeTimeAtSlot(config, update.signatureSlot + ONE_INTERVAL_OF_SLOT, genesisTime) * 1000;
const earliestAllowedTimestampMs = signatureSlot13TimestampMs - MAXIMUM_GOSSIP_CLOCK_DISPARITY;
return Date.now() < earliestAllowedTimestampMs;
}
27 changes: 19 additions & 8 deletions packages/beacon-node/src/metrics/validatorMonitor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
computeEpochAtSlot,
computeTimeAtSlotInterval,
parseAttesterFlags,
CachedBeaconStateAllForks,
CachedBeaconStateAltair,
parseParticipationFlags,
computeStartSlotAtEpoch,
getBlockRootAtSlot,
ParticipationFlags,
SlotInterval,
} from "@lodestar/state-transition";
import {LogData, LogHandler, LogLevel, Logger, MapDef, MapDefMax, toRootHex} from "@lodestar/utils";
import {BeaconBlock, RootHex, altair, deneb} from "@lodestar/types";
Expand Down Expand Up @@ -384,7 +386,8 @@ export function createValidatorMonitor(
registerBeaconBlock(src, seenTimestampSec, block) {
const validator = validators.get(block.proposerIndex);
// Returns the delay between the start of `block.slot` and `seenTimestamp`.
const delaySec = seenTimestampSec - (genesisTime + block.slot * config.SECONDS_PER_SLOT);
const delaySec =
seenTimestampSec - computeTimeAtSlotInterval(config, block.slot, SlotInterval.BLOCK_PROPAGATION, genesisTime);
metrics.gossipBlock.elapsedTimeTillReceived.observe(delaySec);
if (validator) {
metrics.validatorMonitor.beaconBlockTotal.inc({src});
Expand Down Expand Up @@ -428,7 +431,9 @@ export function createValidatorMonitor(
onPoolSubmitUnaggregatedAttestation(seenTimestampSec, indexedAttestation, subnet, sentPeers) {
const data = indexedAttestation.data;
// Returns the duration between when the attestation `data` could be produced (1/3rd through the slot) and `seenTimestamp`.
const delaySec = seenTimestampSec - (genesisTime + (data.slot + 1 / 3) * config.SECONDS_PER_SLOT);
const delaySec =
seenTimestampSec -
computeTimeAtSlotInterval(config, data.slot, SlotInterval.ATTESTATION_PROPAGATION, genesisTime);
for (const index of indexedAttestation.attestingIndices) {
const validator = validators.get(index);
if (validator) {
Expand Down Expand Up @@ -460,8 +465,10 @@ export function createValidatorMonitor(
const src = OpSource.gossip;
const data = indexedAttestation.data;
const epoch = computeEpochAtSlot(data.slot);
// Returns the duration between when the attestation `data` could be produced (1/3rd through the slot) and `seenTimestamp`.
const delaySec = seenTimestampSec - (genesisTime + (data.slot + 1 / 3) * config.SECONDS_PER_SLOT);
// Returns the duration between when the attestation `data` could be produced (one interval through the slot) and `seenTimestamp`.
const delaySec =
seenTimestampSec -
computeTimeAtSlotInterval(config, data.slot, SlotInterval.ATTESTATION_PROPAGATION, genesisTime);

for (const index of indexedAttestation.attestingIndices) {
const validator = validators.get(index);
Expand All @@ -477,8 +484,10 @@ export function createValidatorMonitor(

onPoolSubmitAggregatedAttestation(seenTimestampSec, indexedAttestation, sentPeers) {
const data = indexedAttestation.data;
// Returns the duration between when a `AggregateAndproof` with `data` could be produced (2/3rd through the slot) and `seenTimestamp`.
const delaySec = seenTimestampSec - (genesisTime + (data.slot + 2 / 3) * config.SECONDS_PER_SLOT);
// Returns the duration between when a `AggregateAndproof` with `data` could be produced (two intervals through the slot) and `seenTimestamp`.
const delaySec =
seenTimestampSec -
computeTimeAtSlotInterval(config, data.slot, SlotInterval.AGGREGATION_PROPAGATION, genesisTime);

for (const index of indexedAttestation.attestingIndices) {
const validator = validators.get(index);
Expand All @@ -504,8 +513,10 @@ export function createValidatorMonitor(
const src = OpSource.gossip;
const data = indexedAttestation.data;
const epoch = computeEpochAtSlot(data.slot);
// Returns the duration between when a `AggregateAndProof` with `data` could be produced (2/3rd through the slot) and `seenTimestamp`.
const delaySec = seenTimestampSec - (genesisTime + (data.slot + 2 / 3) * config.SECONDS_PER_SLOT);
// Returns the duration between when a `AggregateAndProof` with `data` could be produced (two intervals through the slot) and `seenTimestamp`.
const delaySec =
seenTimestampSec -
computeTimeAtSlotInterval(config, data.slot, SlotInterval.AGGREGATION_PROPAGATION, genesisTime);

const aggregatorIndex = signedAggregateAndProof.message.aggregatorIndex;
const validatorAggregator = validators.get(aggregatorIndex);
Expand Down
14 changes: 7 additions & 7 deletions packages/beacon-node/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from "@lodestar/types";
import {routes} from "@lodestar/api";
import {ResponseIncoming} from "@lodestar/reqresp";
import {ForkSeq, MAX_BLOBS_PER_BLOCK} from "@lodestar/params";
import {ForkSeq, INTERVALS_PER_SLOT, MAX_BLOBS_PER_BLOCK} from "@lodestar/params";
import {Metrics, RegistryMetricCreator} from "../metrics/index.js";
import {IBeaconChain} from "../chain/index.js";
import {IBeaconDb} from "../db/interface.js";
Expand Down Expand Up @@ -588,9 +588,9 @@ export class Network implements INetwork {
// TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember())

try {
// messages SHOULD be broadcast after one-third of slot has transpired
// messages SHOULD be broadcast after one interval of slot has transpired
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#sync-committee
await this.waitOneThirdOfSlot(finalityUpdate.signatureSlot);
await this.waitOneIntervalOfSlot(finalityUpdate.signatureSlot);
await this.publishLightClientFinalityUpdate(finalityUpdate);
} catch (e) {
// Non-mandatory route on most of network as of Oct 2022. May not have found any peers on topic yet
Expand All @@ -605,9 +605,9 @@ export class Network implements INetwork {
// TODO: Review is OK to remove if (this.hasAttachedSyncCommitteeMember())

try {
// messages SHOULD be broadcast after one-third of slot has transpired
// messages SHOULD be broadcast after one interval of slot has transpired
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#sync-committee
await this.waitOneThirdOfSlot(optimisticUpdate.signatureSlot);
await this.waitOneIntervalOfSlot(optimisticUpdate.signatureSlot);
await this.publishLightClientOptimisticUpdate(optimisticUpdate);
} catch (e) {
// Non-mandatory route on most of network as of Oct 2022. May not have found any peers on topic yet
Expand All @@ -618,8 +618,8 @@ export class Network implements INetwork {
}
};

private waitOneThirdOfSlot = async (slot: number): Promise<void> => {
const secAtSlot = computeTimeAtSlot(this.config, slot + 1 / 3, this.chain.genesisTime);
private waitOneIntervalOfSlot = async (slot: number): Promise<void> => {
const secAtSlot = computeTimeAtSlot(this.config, slot + 1 / INTERVALS_PER_SLOT, this.chain.genesisTime);
const msToSlot = secAtSlot * 1000 - Date.now();
await sleep(msToSlot, this.controller.signal);
};
Expand Down
1 change: 1 addition & 0 deletions packages/state-transition/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from "./shufflingDecisionRoot.js";
export * from "./signatureSets.js";
export * from "./signingRoot.js";
export * from "./slot.js";
export * from "./slotInterval.js";
export * from "./syncCommittee.js";
export * from "./validator.js";
export * from "./weakSubjectivity.js";
Expand Down
11 changes: 11 additions & 0 deletions packages/state-transition/src/util/slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {ChainConfig} from "@lodestar/config";
import {GENESIS_SLOT, INTERVALS_PER_SLOT} from "@lodestar/params";
import {Slot, Epoch, TimeSeconds} from "@lodestar/types";
import {computeStartSlotAtEpoch, computeEpochAtSlot} from "./epoch.js";
import {SlotInterval, calculateSlotWithInterval} from "./slotInterval.js";

export function getSlotsSinceGenesis(config: ChainConfig, genesisTime: TimeSeconds): Slot {
const diffInSeconds = Date.now() / 1000 - genesisTime;
Expand All @@ -21,6 +22,16 @@ export function computeTimeAtSlot(config: ChainConfig, slot: Slot, genesisTime:
return genesisTime + slot * config.SECONDS_PER_SLOT;
}

/** Compute time in seconds of the beginnng of interval of slot */
export function computeTimeAtSlotInterval(
config: ChainConfig,
slot: Slot,
interval: SlotInterval,
genesisTime: TimeSeconds
): TimeSeconds {
return genesisTime + calculateSlotWithInterval(slot, interval) * config.SECONDS_PER_SLOT;
}

export function getCurrentInterval(config: ChainConfig, secondsIntoSlot: number): number {
const timePerInterval = Math.floor(config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT);
return Math.floor(secondsIntoSlot / timePerInterval);
Expand Down
39 changes: 39 additions & 0 deletions packages/state-transition/src/util/slotInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {INTERVALS_PER_SLOT} from "@lodestar/params";
import {Slot} from "@lodestar/types";
/**
* SlotInterval defines intervals within a slot.
* For example, attestation propagation happens at the beginning of interval 1 (second interval) of the slot
* whereas block propagation happens at the beginning of interval 0 (first interval) of the slot.
*
* This can also be interpret as seconds into the slot.
* For example, block propagation happens at (0 * SECONDS_PER_SLOT / INTERVALS_PER_SLOT) seconds into the slot
* and attestation propagation happens (1 * SECONDS_PER_SLOT / INTERVALS_PER_SLOT) seconds into the slot
*
* Some intervals might have several validator actions eg. aggregate and sync aggregate both happening at the beginning
* inteval 2.
*/
/* eslint-disable @typescript-eslint/no-duplicate-enum-values */
export enum SlotInterval {
BLOCK_PROPAGATION = 0,
Copy link
Member

Choose a reason for hiding this comment

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

Wondering if the name here is accurate because the interval includes block production (local and builder) while propagation (via gossipsub) of the block a lot of the time happens at 2 seconds or later into the slot.

SYNC_ATTESTATION_PROPAGATION = 0,
BEACON_COMMITTEE_SELECTION = 0,
ATTESTATION_PROPAGATION = 1,
SYNC_COMMITTEE_SELECTION = 1,
AGGREGATION_PROPAGATION = 2,
SYNC_AGGREGATE_PROPAGATION = 2,
}

export const ONE_INTERVAL_OF_SLOT = 1 / INTERVALS_PER_SLOT;

/** Return a decimal slot given slot and interval */
export function calculateSlotWithInterval(slot: Slot, interval: SlotInterval): Slot {
return slot + getSlotFractionFromInterval(interval);
}

/** Return a fraction of a slot given interval */
export function getSlotFractionFromInterval(interval: SlotInterval): Slot {
return interval / INTERVALS_PER_SLOT;
}
export function endOfInterval(interval: SlotInterval): Slot {
return interval + 1;
}
4 changes: 2 additions & 2 deletions packages/validator/src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function getMetrics(register: MetricsRegisterExtra, gitData: LodestarGitD
syncCommitteeStepCallProduceMessage: register.histogram({
name: "vc_sync_committee_step_call_produce_message_seconds",
help: "Time between 1/3 of slot and call produce message",
// Max wait time is 1 / 3 of slot
// Max wait time is 1 / INTERVALS_PER_SLOT of slot
buckets: [0.5, 1, 2, 3, 6, 12],
}),
syncCommitteeStepCallPublishMessage: register.histogram({
Expand All @@ -86,7 +86,7 @@ export function getMetrics(register: MetricsRegisterExtra, gitData: LodestarGitD
syncCommitteeStepCallProduceAggregate: register.histogram({
name: "vc_sync_committee_step_call_produce_aggregate_seconds",
help: "Time between 2/3 of slot and call produce aggregate",
// Min wait time is 2 / 3 of slot
// Min wait time is 2 / INTERVAL_PER_SLOTS of slot
buckets: [0.5, 1, 2, 3, 6, 12],
}),
syncCommitteeStepCallPublishAggregate: register.histogram({
Expand Down
Loading
Loading