Forward epoch sync hardening — not a hardfork
Gravity v1.9.3 backports the forward-epoch-sync client/serving hardening from main (#834 → #839) onto the v1.9 line. It only changes how a node that is behind the chain fetches and verifies committed blocks from its peers, and how a serving node builds and admits those requests.
This release is not a hardfork and does not change any consensus or execution rule:
- No hardfork schedule change. Mainnet Beta / Osaka stay at
1787018400(2026-08-18 02:00:00 UTC) exactly as in v1.9.1 / v1.9.2. - No consensus-rule, state-transition, or block-format change. A v1.9.3 node produces and validates byte-identical blocks and state to a v1.9.2 node.
- No ConsensusDB schema change, no genesis change.
- No
gravity-reth/gravity-aptospin change. - Wire-compatible with v1.9.2 for existing tags. One new error variant is appended (
ForwardEpochSyncError::Disabled, BCS tag7); older tags keep their numbers. Mixed v1.9.2 / v1.9.3 fleets interoperate — a peer that rejects or times out still drives the client toward another peer or legacy reverse sync when appropriate. - Opt-in and default off. Runtime behaviour is unchanged unless
ENABLE_FORWARD_EPOCH_SYNC=trueis set.
Upgrading is optional and can be rolled out node by node — there is no coordinated activation and no deadline. Operators already running forward sync on v1.9.2 should upgrade: v1.9.2 treated the first Busy as a reason to fall back to legacy, which is what this release fixes.
What's changed
Retry on Busy, bound by progress — not by one peer (#834 → #839)
Follow-up to the batched forward epoch sync shipped in v1.9.2 (#819 / #832 → #833).
Client
- Prepare failures are classified instead of all mapping to legacy:
Busy— peer stays in the candidate list; the pass finishes the other candidates first; passes that sawBusyback off 0.5 / 1 / 2 s (±25 % jitter), at most 3 times, then the attempt returnsResumeso the next epoch-change trigger asks again — never legacy forBusyalone.- Explicit rejection (
Disabled,EpochNotFound,AnchorMismatch,Internal, …) — peer removed; legacy only once the list is empty. - RPC timeout / error — one more probe for that peer (a timed-out probe waits 5 s before the re-ask), then removed.
- One Prepare attempt is capped at 90 s (nine 10 s probes). At the cap:
Busyseen →Resume, otherwise legacy. Mixed fleets therefore reach a decision within 90 s regardless of peer count. - Fetch progress watchdog: every 60 s window must deliver ≥ 600 blocks (judged at page boundaries); a failed window returns
Resume.Busyduring Fetch back offs 0.5 → 8 s (±25 % jitter) on the same serving peer, never past the window. EpochSyncOutcome::Resumekeeps everything already persisted and replayed; the next trigger re-Prepares from the advancedordered_root. Logged at info; does not bumpERROR_COUNT.- Prepare timeout is fixed to the network inbound cap (10 s). The v1.9.2 env override
FORWARD_EPOCH_SYNC_PREPARE_TIMEOUT_MSECis removed — waiting longer cannot buy more server work once the network layer has dropped the RPC.
Serving
- Feature off → new error
Disabled; feature on but RoundManager not ready →Busy(both wereInternalbefore). - Cold index builds run off the RPC path (
spawn_blocking, singleflight join, index LRU of 4 epochs). A Prepare whose build is still running 8 s after the request was received answersBusyin band and keeps building. - Quotas (default 4, env-overridable):
FORWARD_EPOCH_SYNC_COLD_BUILD_QUOTA(concurrent cold builds) andFORWARD_EPOCH_SYNC_FETCH_QUOTA(concurrent Fetch handlers / admitted peers). - One Prepare handler and one Fetch handler per peer, held through BCS encoding and hand-off to the network layer.
RoundManager races the sync against close_rx, so shutdown no longer waits for an in-flight sync.
Operator notes
- Default behaviour is unchanged when the flag is unset: legacy reverse epoch sync, same as v1.9.1 / v1.9.2 without the flag.
- Enabling: set
ENABLE_FORWARD_EPOCH_SYNC=truein thegravity_nodeprocess environment.- On the syncing node it selects the forward path (
Busy/ progress stalls →Resumeand retry; legacy only after every candidate rejected or never answered). - On a serving node it starts the forward-sync serving task. A serving node without the flag answers
Disabled; the client moves on. - For the forward path to actually be used, both the catching-up node and at least one upstream peer must have the flag set.
- On the syncing node it selects the forward path (
- Recommended for: VFNs / PFNs catching up many epochs, and the validators / VFNs that serve them — especially any fleet that saw v1.9.2 fall back to legacy on the first
Busy. - Tuning (new / changed vs v1.9.2):
FORWARD_EPOCH_SYNC_COLD_BUILD_QUOTA(default4) — concurrent cold index builds on a serving node.FORWARD_EPOCH_SYNC_FETCH_QUOTA(default4) — concurrent Fetch handlers (one per peer, node-wide cap).- Invalid / out-of-range values fall back to
4with a warning. FORWARD_EPOCH_SYNC_PREPARE_TIMEOUT_MSECis gone — Prepare timeout is hard-wired to 10 s (network inbound cap). Remove it from env files if present; it has no effect.
- No restart ordering, no coordinated upgrade window. Nodes can be upgraded (and the flag enabled) one at a time.
Docker image
The release pipeline publishes:
docker pull ghcr.io/galxe/gravity_node:v1.9.3Dependency revision
gravity-reth: unchanged atbc817c642c9c3816cc4e22754e13e3c9633419ddgravity-aptos: unchanged atb1f68dc85781ef0d28a568d9d64604b153be9d9e
Full changelogs
- gravity-sdk v1.9.2...v1.9.3
- Backport PR: #839 (cherry-pick of #834 from
main)