Slots & Epochs

Solana organizes time into discrete units called slots and epochs. Understanding this temporal structure is essential for grasping how Solana achieves its performance while maintaining decentralization.

Time Structure Overview

Text
┌─────────────────────────────────────────────────────────────────────────┐
│                             EPOCH                                       │
│                         432,000 slots                                   │
│                          (~2-3 days)                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌────────────────┐ ┌────────────────┐       ┌────────────────┐        │
│  │    Slot 0      │ │    Slot 1...Slot 431,999  │        │
│  │    (~400ms)    │ │    (~400ms)    │       │    (~400ms)    │        │
│  └────────────────┘ └────────────────┘       └────────────────┘        │
│                                                                         │
│  Leader: Validator A   Leader: Validator B     Leader: Validator X     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Within each slot:
┌─────────────────────────────────────────────────────────────────────────┐
│                              SLOT                                       │
│                           ~400ms                                        │
│                         64 ticks                                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Tick 0   Tick 1   Tick 2  ...  Tick 62   Tick 63                      │
│  [  *  ]  [  *  ]  [  *  ]      [   *  ]  [   *  ]                     │
│                                                                         │
│  Each tick = ~6.25ms                                                    │
│  Each tick = ~6,250 PoH hashes                                         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Slots

A slot is the basic unit of time in Solana, lasting approximately 400 milliseconds.

Slot Properties

TypeScript
interface Slot {
  // Timing
  targetDuration: number; // ~400ms
  ticksPerSlot: number; // 64
  hashesPerTick: number; // ~6,250

  // Leader assignment
  leader: PublicKey; // Who produces this slot

  // Block content
  entries: Entry[]; // Transactions + PoH hashes
  parentSlot: number; // Previous slot
  blockhash: string; // Hash of block contents

  // State
  status: "processed" | "confirmed" | "finalized" | "skipped";
}

Slot Lifecycle

Text
SLOT LIFECYCLE
══════════════

1. SCHEDULED
   └── Leader knows it's their turn
   └── Prepares to receive transactions

2. PRODUCING (Leader active)
   ├── Receives transactions via TPU
   ├── Executes transactions
   ├── Creates PoH entries
   └── Broadcasts to validators

3. PROCESSED
   └── Block exists but not yet voted on
   └── May still be rolled back

4. CONFIRMED
   └── Supermajority of stake voted
   └── Very unlikely to roll back

5. FINALIZED
   └── Maximum lockout reached
   └── Cannot be rolled back

Special case:
SKIPPED
   └── Leader failed to produce block
   └── Timeout reached
   └── Slot is empty

Querying Slot Information

TypeScript
import { Connection } from "@solana/web3.js";

const connection = new Connection("https://api.mainnet-beta.solana.com");

// Current slot
const currentSlot = await connection.getSlot();
console.log("Current slot:", currentSlot);

// Slot with specific commitment
const confirmedSlot = await connection.getSlot("confirmed");
const finalizedSlot = await connection.getSlot("finalized");

// Slot info
const slot = await connection.getSlot();
const leader = await connection.getSlotLeader();
console.log(`Slot ${slot} leader: ${leader}`);

// Block time (when slot was produced)
const blockTime = await connection.getBlockTime(slot);
console.log("Block time:", new Date(blockTime! * 1000));

// Get block data
const block = await connection.getBlock(slot, {
  maxSupportedTransactionVersion: 0,
});
if (block) {
  console.log("Transactions:", block.transactions.length);
  console.log("Blockhash:", block.blockhash);
  console.log("Parent slot:", block.parentSlot);
}

Skipped Slots

Slots can be skipped when leaders fail to produce blocks:

TypeScript
// Check slot status
async function getSlotStatus(
  connection: Connection,
  slot: number,
): Promise<"produced" | "skipped" | "future"> {
  const currentSlot = await connection.getSlot();

  if (slot > currentSlot) {
    return "future";
  }

  try {
    const block = await connection.getBlock(slot, {
      maxSupportedTransactionVersion: 0,
    });
    return block ? "produced" : "skipped";
  } catch {
    return "skipped";
  }
}

// Get skip rate for a range
async function calculateSkipRate(
  connection: Connection,
  startSlot: number,
  count: number,
): Promise<number> {
  let skipped = 0;
  let total = 0;

  for (let i = 0; i < count; i++) {
    const slot = startSlot + i;
    const status = await getSlotStatus(connection, slot);
    if (status !== "future") {
      total++;
      if (status === "skipped") skipped++;
    }
  }

  return skipped / total;
}

Epochs

An epoch is a longer time period containing 432,000 slots (~2-3 days).

Why Epochs Exist

Text
EPOCH PURPOSES
══════════════

1. LEADER SCHEDULE GENERATION
   └── At epoch start, entire epoch's schedule is determined
   └── Based on stake distribution at epoch boundary
   └── Predictable 2-3 days in advance

2. STAKE ACTIVATION/DEACTIVATION
   └── Stake changes take effect at epoch boundaries
   └── Prevents rapid stake manipulation
   └── Provides stability period

3. REWARDS DISTRIBUTION
   └── Inflation rewards calculated per epoch
   └── Vote credits tallied
   └── Rewards distributed at epoch end

4. PROTOCOL FEATURES
   └── Some features activate at epoch boundaries
   └── Feature gates checked per epoch

Epoch Information

TypeScript
// Get epoch info
const epochInfo = await connection.getEpochInfo();

console.log("Epoch info:", {
  epoch: epochInfo.epoch,
  slotIndex: epochInfo.slotIndex, // Current slot within epoch
  slotsInEpoch: epochInfo.slotsInEpoch, // 432,000
  absoluteSlot: epochInfo.absoluteSlot, // Total slots since genesis
  blockHeight: epochInfo.blockHeight, // Confirmed blocks
  transactionCount: epochInfo.transactionCount,
});

// Calculate epoch progress
const epochProgress = epochInfo.slotIndex / epochInfo.slotsInEpoch;
console.log(
  `Epoch ${epochInfo.epoch} is ${(epochProgress * 100).toFixed(2)}% complete`,
);

// Estimate time remaining in epoch
const slotsRemaining = epochInfo.slotsInEpoch - epochInfo.slotIndex;
const msRemaining = slotsRemaining * 400; // ~400ms per slot
const hoursRemaining = msRemaining / (1000 * 60 * 60);
console.log(`~${hoursRemaining.toFixed(1)} hours until next epoch`);

Epoch Schedule

TypeScript
// Get epoch schedule
const epochSchedule = await connection.getEpochSchedule();

console.log("Epoch schedule:", {
  slotsPerEpoch: epochSchedule.slotsPerEpoch, // 432,000
  leaderScheduleSlotOffset: epochSchedule.leaderScheduleSlotOffset,
  warmup: epochSchedule.warmup, // Warmup period enabled
  firstNormalEpoch: epochSchedule.firstNormalEpoch,
  firstNormalSlot: epochSchedule.firstNormalSlot,
});

// Get first slot of an epoch
const epoch = epochInfo.epoch;
const firstSlot = epochSchedule.getFirstSlotInEpoch(epoch);
const lastSlot = epochSchedule.getLastSlotInEpoch(epoch);

console.log(`Epoch ${epoch}: slots ${firstSlot} - ${lastSlot}`);

Leader Schedule

The leader schedule determines which validator produces blocks for each slot.

Schedule Generation

Text
LEADER SCHEDULE ALGORITHM
═════════════════════════

Input:
├── Stake distribution at epoch boundary
├── Random seed from recent PoH
└── Slots per epoch (432,000)

Process:
1. Collect all validators with active stake
2. Weight each validator by stake amount
3. Use PoH-derived seed for randomness
4. Assign slots proportional to stake

Output:
├── 432,000 slot assignments
├── Each slot has exactly one leader
└── Published at epoch start

Example distribution (simplified):
Validator A (30% stake)~129,600 slots
Validator B (20% stake)~86,400 slots
Validator C (15% stake)~64,800 slots
...

Querying Leader Schedule

TypeScript
// Get leader schedule for current epoch
const leaderSchedule = await connection.getLeaderSchedule();

if (leaderSchedule) {
  // leaderSchedule is a map: validator => [slot indices]
  for (const [validator, slots] of Object.entries(leaderSchedule)) {
    console.log(`${validator}: ${slots.length} slots`);
  }
}

// Get upcoming slot leaders
const currentSlot = await connection.getSlot();
const leaders = await connection.getSlotLeaders(currentSlot, 10);

console.log("Next 10 slot leaders:");
leaders.forEach((leader, i) => {
  console.log(`  Slot ${currentSlot + i}: ${leader.toBase58()}`);
});

// Get a specific slot's leader
const slotLeader = await connection.getSlotLeader();
console.log("Current leader:", slotLeader.toBase58());

Leader Schedule Lookahead

TypeScript
// Leaders know their schedule ahead of time
async function getMyUpcomingSlots(
  connection: Connection,
  myValidatorId: PublicKey,
): Promise<number[]> {
  const epochInfo = await connection.getEpochInfo();
  const leaderSchedule = await connection.getLeaderSchedule();

  if (!leaderSchedule) return [];

  const mySlots = leaderSchedule[myValidatorId.toBase58()] || [];

  // Filter to upcoming slots only
  const upcomingSlots = mySlots.filter(
    (slotIndex) => slotIndex > epochInfo.slotIndex,
  );

  // Convert to absolute slot numbers
  const epochFirstSlot = epochInfo.absoluteSlot - epochInfo.slotIndex;
  return upcomingSlots.map((index) => epochFirstSlot + index);
}

Slots, Blocks, and Confirmations

Important distinctions:

Text
TERMINOLOGY CLARITY
═══════════════════

SLOTBLOCK

Slot:
├── Time period (~400ms)
├── Always exists (time keeps moving)
└── May or may not contain a block

Block:
├── Data structure with transactions
├── Created by leader during slot
└── Can be missing if slot skipped

SLOT NUMBER vs BLOCK HEIGHT

Slot 100Block might not exist (skipped)
Slot 101Block exists (height 95)
Slot 102Block exists (height 96)
Slot 103Block might not exist (skipped)
Slot 104Block exists (height 97)

Block height only counts CONFIRMED blocks
Slot number counts ALL time periods

Confirmation Depth

TypeScript
// Calculate confirmation depth
async function getConfirmationDepth(
  connection: Connection,
  slot: number,
): Promise<{
  slotDepth: number;
  blockDepth: number;
  status: string;
}> {
  const currentSlot = await connection.getSlot("confirmed");
  const slotDepth = currentSlot - slot;

  // Block height at target slot
  let targetBlock;
  try {
    targetBlock = await connection.getBlock(slot, {
      maxSupportedTransactionVersion: 0,
    });
  } catch {
    return {
      slotDepth,
      blockDepth: -1,
      status: "skipped",
    };
  }

  if (!targetBlock) {
    return {
      slotDepth,
      blockDepth: -1,
      status: "not_found",
    };
  }

  const currentBlockHeight = await connection.getBlockHeight("confirmed");
  const blockDepth = currentBlockHeight - (targetBlock.blockHeight || 0);

  let status = "processed";
  if (slotDepth >= 32) status = "finalized";
  else if (slotDepth >= 1) status = "confirmed";

  return { slotDepth, blockDepth, status };
}

Recent Blockhashes

Transactions require a recent blockhash for:

  1. Preventing replay attacks
  2. Providing transaction expiry
TypeScript
// Get recent blockhash
const { blockhash, lastValidBlockHeight } =
  await connection.getLatestBlockhash();

console.log("Blockhash:", blockhash);
console.log("Valid until block:", lastValidBlockHeight);

// Blockhash validity
/*
A blockhash is valid for approximately:
├── ~150 blocks (~60 seconds)
├── lastValidBlockHeight indicates exact expiry
└── After expiry, transaction is invalid

Timeline:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  Block 1000    Block 1050    Block 1100    Block 1150          │
│     │             │             │             │                 │
│     ▼             │             │             │                 │
│  [Blockhash]      │             │             │                 │
│  obtained         │             │             ▼                 │
│                   │             │         [Expired]            │
│                   ▼             ▼                               │
│              [Still valid] [Getting stale]                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
*/

Handling Blockhash Expiry

TypeScript
async function sendTransactionWithRetry(
  connection: Connection,
  transaction: Transaction,
  signers: Keypair[],
): Promise<string> {
  const maxRetries = 3;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    // Get fresh blockhash each attempt
    const { blockhash, lastValidBlockHeight } =
      await connection.getLatestBlockhash("confirmed");

    transaction.recentBlockhash = blockhash;
    transaction.feePayer = signers[0].publicKey;

    // Sign with fresh blockhash
    transaction.sign(...signers);

    const signature = await connection.sendRawTransaction(
      transaction.serialize(),
    );

    // Wait for confirmation
    const confirmation = await connection.confirmTransaction({
      signature,
      blockhash,
      lastValidBlockHeight,
    });

    if (confirmation.value.err === null) {
      return signature;
    }

    console.log(`Attempt ${attempt + 1} failed, retrying...`);
  }

  throw new Error("Transaction failed after max retries");
}

Practical Applications

Timing-Sensitive Operations

TypeScript
// Wait for specific epoch
async function waitForEpoch(
  connection: Connection,
  targetEpoch: number,
): Promise<void> {
  while (true) {
    const epochInfo = await connection.getEpochInfo();

    if (epochInfo.epoch >= targetEpoch) {
      return;
    }

    const slotsToWait = epochInfo.slotsInEpoch - epochInfo.slotIndex;
    const msToWait = slotsToWait * 400;

    console.log(`Waiting ${msToWait / 1000}s for epoch ${targetEpoch}...`);
    await new Promise((r) => setTimeout(r, Math.min(msToWait, 60000)));
  }
}

// Wait for finalization
async function waitForFinalization(
  connection: Connection,
  signature: string,
  targetSlot: number,
): Promise<void> {
  const FINALIZATION_SLOTS = 32;

  while (true) {
    const finalizedSlot = await connection.getSlot("finalized");

    if (finalizedSlot >= targetSlot) {
      return;
    }

    const slotsRemaining = targetSlot - finalizedSlot + FINALIZATION_SLOTS;
    console.log(`~${slotsRemaining} slots until finalized`);

    await new Promise((r) => setTimeout(r, 5000));
  }
}

Epoch-Based Scheduling

TypeScript
// Schedule action for next epoch
async function scheduleForNextEpoch(
  connection: Connection,
  action: () => Promise<void>,
): Promise<void> {
  const epochInfo = await connection.getEpochInfo();
  const targetEpoch = epochInfo.epoch + 1;

  console.log(`Scheduling action for epoch ${targetEpoch}`);

  // Calculate wait time
  const slotsRemaining = epochInfo.slotsInEpoch - epochInfo.slotIndex;
  const msToWait = slotsRemaining * 400;

  // Wait with periodic checks
  while (true) {
    const current = await connection.getEpochInfo();

    if (current.epoch >= targetEpoch) {
      console.log(`Epoch ${targetEpoch} started, executing action`);
      await action();
      return;
    }

    await new Promise((r) => setTimeout(r, 10000));
  }
}

Key Formulas

Text
Time Calculations
═════════════════

Slot duration:
  slot_ms = 400ms (target)

Ticks per slot:
  ticks = 64

Epoch duration:
  epoch_slots = 432,000
  epoch_ms = 432,000 × 400 = 172,800,000ms
  epoch_hours ≈ 48 hours
  epoch_days ≈ 2 days

Leader slots per epoch (for validator with X% stake):
  leader_slots ≈ 432,000 × X%

Block height vs slot:
  block_height ≤ slot_number
  (equals only if no slots skipped)

Finalization time:
  finalization_slots ≈ 32
  finalization_ms ≈ 32 × 400 = 12,800ms ≈ 13 seconds

Key Takeaways

  1. Slots are ~400ms time periods; blocks are data created in slots
  2. Epochs are 432,000 slots (~2 days) used for schedule and rewards
  3. Leader schedule is determined by stake, generated per epoch
  4. Skipped slots occur when leaders fail; block height < slot number
  5. Blockhashes expire after ~150 blocks; always use fresh ones

Next: Leader Rotation - How Solana selects and rotates block producers.