When a snap pivot moves in xOne, the node picks a new pivot block, keeps the state rows it already downloaded and repairs them to the new state root. xOneRS does have a trie-healing step: it fetches the missing trie nodes with GetTrieNodes and checks each node by its hash. Nothing is installed until the rebuilt root equals the state root of the last pivot.
This post describes xOneRS main today and what the xOneTS and xOneC docs say. The xOneTS and xOneC repositories are private. For the full picture, read how snap, fast and full sync work in xOne.
How does xOne choose a snap pivot?
The pivot is the block head - 64, lowered to the first block whose anchor verifies. The anchor is the epoch-switch header's masternode list, the quorum certificates of the next three blocks, and the 3-chain that commits the pivot (pivot rule).
- The switch header is pinned by
--snap.pins <file>, or by at least two peers that serve the same hash. A peer on another chain is outvoted and logged. - A pivot on a branch that fails
--eth.requiredblocksis refused before state is requested (peer chain checks).
Why does the pivot move?
The pivot moves because peers keep state for recent roots only. The spec says a geth path-scheme node serves about the last 128 blocks, about 4 minutes on a 2-second chain. A longer download gets "no data" from every peer.
In the client, two empty answers in a row give up the root (max_empty: 2 in Config::default). The error is SyncError::Unavailable. Before PR #143, this error ended the sync and the node restarted from an empty datadir.
What does xOneRS do when the pivot moves?
xOneRS re-pivots. The driver in snapsync.rs runs three steps, up to 64 times (MAX_REPIVOTS):
- Pick a fresh pivot by the same rules: new tip,
head - 64, anchor verified, switch header pinned again. Fetch the header window, bodies, receipts and gap pools again for it. - Call
StateStore::set_root(new root). The downloaded accounts, storage and codes stay. The cursors go on. - Run
client::syncagain. It reconciles, downloads the rest, heals and checks the root.
Each move logs pivot moved to root ... (move n). After 64 moves the sync fails with the last error.
Does xOneRS heal the state trie?
Yes. The heal pass is part of client::sync in crates/xone-snap. It uses the snap/1 message GetTrieNodes (code 0x06), which asks for a node by its path (messages.md).
- Reconcile first. After a move, the client walks the account trie at the new root. An account that is gone is dropped with its storage. An account whose storage root changed starts its storage over. geth answers a storage request for an unknown account with an empty reply, so this step comes first.
- Walk and fetch. The client walks the tries from the new root over the nodes it can build from local rows plus fetched nodes. It asks for each missing node by path, 1,024 paths a request (
Config::default). - Check by hash. A node is kept only if its hash is the one the walk needs. An empty string in the answer is a gap.
- Stop. The walk ends when no node is missing. Its leaves replace the local rows. Rounds are bounded by
max_heal_rounds(128).
A storage trie whose root the local slots already hash to is left alone. The net2040 docs show runs with 0 trie nodes (heal) (net2040-sync-modes.md).
What does xOneRS not do?
The code names its own limit. Each heal round walks and rebuilds from the whole store. Geth keeps a persistent frontier of paths. xOneRS does not, and it does not keep fetched nodes across runs (code comment above heal in client.rs).
How is a moved pivot still checked?
Every range is proven against the root with verify_range, and a range with no proof is refused unless its leaves alone rebuild the whole trie (range-proofs.md). The root rebuilt from the hashed rows must equal the pivot's state root.
The sync is not done when the state is done. The first block after the pivot can be an epoch switch, and its reward hook reads data below the pivot. client::finish refuses with SyncError::Incomplete until every range of the plan is held (completeness.md). The anchor, window and state of the installed datadir belong to one pivot.
What do the other xOne clients do?
xOneTS and xOneC each have a moving-pivot step. Details differ.
| Client | What the repository says | Source |
|---|---|---|
| xOneRS | Pivot head - 64. Re-pivot on Unavailable, up to 64 moves. Reconcile, then GetTrieNodes heal. |
pivot.md |
| xOneTS (private) | Fresh anchor-verified pivot when the root goes stale. Keeps rows. Heals accounts with GetTrieNodes. Re-downloads storage of accounts whose storage root moved. Listed under Unreleased. |
CHANGELOG |
| xOneC (private) | Downloads at a root 32 blocks below the tip. Then picks a final pivot 16 blocks below the tip and heals it by path. If peers lose that root, it picks a fresh pivot and the heal goes on. | net2040-snap.md |
The xOneC doc records a net2040 run of 2026-11-13 from two geth-XDC peers: pivot 820125, 109 trie nodes fetched, 12 requests, root verified.
How do other clients describe it?
ethrex describes the same problem. Network and units differ, so the numbers below are not like for like.
| Client | Claim | Network | Source |
|---|---|---|---|
| ethrex | Snap sync downloads leaves, then heals the trie. It re-pivots when a pivot goes stale. About 10 min with snap and heal, against about 45 min with fast sync, as stated in the article of 2026-03-13. | Hoodi | ethrex blog |
| xOneRS | 12 moves in 1945 s. 327,356 accounts, 158,076,210 slots, 39,087 codes, 108 heal requests. Root verified at pivot 85,341,896. | Apothem | PR #143 comment |
The Apothem run stopped before the install: that PR comment records that the install and tip were not reached, because the store was held in memory. A disk-backed download (--syncmode snap --state.disk, PR #148) was merged later. This post claims no Apothem result for it.
Frequently asked questions
Does xOneRS heal state after a snap pivot moves?
Yes. After set_root, the client reconciles the downloaded accounts with the new root, then fetches missing trie nodes with GetTrieNodes and checks each by its hash.
Does a moved pivot throw away the downloaded state?
No. StateStore::set_root keeps the downloaded accounts, storage and codes. An account that is gone is dropped, and an account whose storage root changed starts its storage over.
How many times can the pivot move?
At most 64 times in one sync. The heal is bounded to 128 rounds. Nothing is installed unless the rebuilt root equals the pivot's state root.
Sources
- spec/snap/pivot.md
- spec/snap/range-proofs.md
- spec/snap/completeness.md
- spec/snap/messages.md
- crates/xone-node/src/services/snapsync.rs
- crates/xone-snap/src/client.rs
- docs/ops/net2040-sync-modes.md
- docs/ops/disk-state.md
- xOneRS PR #143: re-pivot when the peers' state window leaves the pivot
- xOneRS PR #148: snap state into the store's tables
- xOneTS CHANGELOG (private repository)
- xOneC docs/ops/net2040-snap.md (private repository)
- How ethrex heals state during snap sync, 2026-03-13
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/snap-pivot-healing/index.md). GitHub may ask you to sign in. Report a correction opens a prefilled GitHub issue for this page.