An XDC epoch switch is the block that opens a new masternode list. In V1 it is a checkpoint at a block number that is a multiple of 900. In V2 it is found by round, so it can be at any block number. xOne derives both kinds of list from contract state and compares them with the lists in the canonical headers.
Each rule below has the geth-XDC file:line from the xOneRS spec pages, at the pin 6fc8e8c2 of XDCIndia/go-ethereum.
How long is an epoch on XDC?
An epoch is 900 units, and the gap is 450. The chain spec of Apothem, mainnet, net2040, devnet and devnet551 each says epoch 900 and gap 450 (forks.json).
The unit differs by version:
- In V1, an epoch is 900 blocks. A checkpoint has
number % epoch == 0andnumber >= epoch. - In V2, an epoch is counted in rounds. A round makes at most one block, so an epoch holds at most 900 blocks.
The V2 switch block is 56,828,700 on Apothem, 80,370,000 on mainnet and 10,800 on net2040. On Apothem, 2,532 consecutive switches (83,000,425 to 85,245,161) are 12 to 900 blocks apart (retention).
What does an epoch-switch header carry?
A V2 switch header carries the masternode list (Validators) and the penalty list (Penalties) in extraData. Any other V2 block must have both lists empty. A V1 checkpoint carries its list in extraData, between a 32-byte vanity and a 65-byte seal.
A verifier (header-verification-v2) checks that Validators has the members of the epoch's masternodes, in the same order from ValidatorOrderBlock (verifyHeader.go:322, :412). Penalties must match the epoch's penalties (verifyHeader.go:423). A non-switch block with a list is rejected (verifyHeader.go:449-455).
How does V2 detect a switch?
V2 detects a switch by round, not by block number. A V2 block is a switch when the round of its QC is below the start round of its own epoch, round - round % epoch (engine_v2/engine.go:1952-2013). The first V2 block is a switch too.
A block number that is a multiple of 900 is usually not a switch. The producer uses isEpochSwitchAtRound for a block that does not exist yet (engine_v2/timeout.go:490-508). Parameters such as maxMasternodes come from the config entry with the highest switch round not above the round (v2-config-by-round, params/config.go:2425-2438).
What are the gap block and the gap snapshot?
The gap block is the block 450 before the epoch start. Its number is number - number % epoch - gap, or 0 when the epoch start is not above gap (engine_v2/engine.go:915-931). The gap snapshot is the candidate pool read from the validator contract 0x88 in the state of that block.
How the pool is built (spec page):
- Read the candidates and the cap of each (
engine.go:2277-2340,core/state/statedb_utils.go:95-130). Drop the zero address. - Sort by cap, largest first, with geth's frozen quicksort (
engine.go:2169-2190,common/sort/slice.go). Equal caps keep the order that sort leaves. A stable sort is wrong at pool width. - Keep the pool whole. No cap and no penalty is applied yet.
The first V2 epoch has no V2 gap block. Its pool is the V1 list in the extraData of the V2 switch block (engine.go:1379-1416).
How are the masternode list and the leader derived?
calcMasternodes removes the penalised candidates from the pool and then cuts at maxMasternodes (engine.go:2023-2100, removal then cut at :2095-2098). V1 cuts first and removes after. The leader of round r is masternodes[r % epoch % len(masternodes)] (verifyHeader.go:480-481, engine_v2/mining.go:24-80).
A candidate that proposes in the middle of an epoch joins the next pool only. The list and its length stay fixed until the next switch. A producer that reads 0x88 again during the epoch desynchronises from the network. The spec names the net5151 incident for this (v2-masternodes-and-leader).
How do V1 checkpoints differ from V2 switches?
A V1 checkpoint is at a fixed block number. The header lists signers by address, but the cap decides who is in (v1-checkpoint-masternodes).
- At the gap block, read the candidates and caps, sort by cap, and keep the first
maxMasternodes(xdpos.go:2135-2177). The limit is 18, ormaxMasternodesV2afterTIPIncreaseMasternodes. Apothem reads 15 from the network constants (network_constants.go:85,:100). - At the checkpoint, remove this epoch's penalties and the
Penaltiesof the checkpoints 1 to 4 epochs back (xdpos.go:1440-1476,constants.go:44). - A verifier compares the list as a multiset (
xdpos.go:1125). On Apothem, geth runs none of these checks (xdpos.go:1210).
A V1 snapshot must restart from the gap block with the candidate list. Votes alone give the wrong signers (v1-snapshot).
Why does a snap node need state below its pivot?
A node that starts from a snapshot has the state of the pivot block P and nothing below it except what it receives. Gap snapshots are not blocks, and geth refuses a snapshot below the pivot after a snap sync (engine.go:1121-1133, issue #1931). So the node needs the gap snapshots in its manifest, not only headers (retention).
For Apothem at pivot 84,000,000, the spec derives these windows:
| class | window |
|---|---|
| headers | 4500 |
| bodies | 1799 |
| gap snapshots | 3149 |
At pivot 83,600,000 the plan lists four gap snapshots, from 83,596,950 to 83,599,650. A rule that needs data below its floor gets a typed Missing error, never an empty list. See sync modes and networks.
How does xOne test epoch switches?
xOne tests them with vectors from the pinned geth-XDC and with replay of recorded blocks. The replay gate post describes the method.
Vectors: gap-apothem.json has 100 switches and gap-net2040.json has 20. sort.json has 30 cases up to 464 candidates, calc.json 19 cases and epoch.json 638 detection cases. Each leader file holds 50 consecutive V2 blocks around a switch. midadd.json has 17 pairs of rounds across an epoch with a new candidate. checkpoints.jsonl has 600 Apothem V1 checkpoints.
Replay (RUNS.md), all at 0 mismatches:
| network | range | epoch switches |
|---|---|---|
| Apothem | 0..=432,000 (2026-11-11) | 480 V1 checkpoint lists derived |
| Apothem | pivot 56,826,900, then to 57,099,999 | 302 V2 switches, 301 gap snapshots |
| net2040 | 30,001..=754,336 (2026-11-12) | 809 V2 switches, 805 gap snapshots |
A mutant that reads the gap snapshot of the previous epoch diverges at Apothem 56,830,293, the third switch of the V2 replay test.
What is not covered yet?
One Apothem replay of 1,000 V2 switches was not reached: the block cache stops at 57,100,000. No mainnet V1 checkpoint vector exists yet. The net5151 vector is synthetic. See the roadmap.
Frequently asked questions
Is an XDC epoch always 900 blocks?
No. The chain spec says 900 on every listed network. In V1 that is 900 blocks. In V2 it is 900 rounds, so an epoch holds at most 900 blocks.
Where is the gap block?
It is 450 blocks before the epoch start: number - number % epoch - gap. The candidate pool is read from state at that block.
Who leads a round in V2?
The leader of round r is masternodes[r % epoch % len(masternodes)], on the list of the block's epoch.
Can a V2 epoch have fewer than 900 blocks?
Yes. On Apothem, 2,532 consecutive switches are 12 to 900 blocks apart.
Sources
- spec/consensus/v2-epoch-switch-and-gap-snapshot.md
- spec/consensus/v2-masternodes-and-leader.md
- spec/consensus/header-verification-v2.md
- spec/consensus/v1-checkpoint-masternodes.md
- spec/consensus/v1-snapshot.md
- spec/chainspec/v2-config-by-round.md
- spec/retention.md
- crates/xone-chainspec/generated/forks.json
- conformance/harness/RUNS.md
- conformance/vectors/
This article describes the source and evidence at its review date. Check the linked repository for current implementation and qualification status.
Explore the engineering evidenceEdit this page on GitHub opens the Markdown source (content/blog/epoch-switches-xdc/index.md). GitHub may ask you to sign in. Report a correction opens a prefilled GitHub issue for this page.