Summary
The --set-head <N> startup flag (env XDC_SET_HEAD, "Rollback chain to block number") overshoots by one block and then fatally aborts startup when N is at/near the node's current committed head. Instead of rewinding to N and continuing, it rewinds to N, N-1, N-2, ... and crashes with:
Fatal: Failed to register the Ethereum service: can't rollback to <N> which is greater than current <N-1>
This makes the flag unusable for its primary purpose (dropping a small uncommitted tail during recovery), and leaves the node down rather than at head N.
Environment
- Client:
XDC v2.7.0-devnet (XDC-v270)
- Datadir restored from a snapshot whose committed head was block 14835 (leveldb contained a non-canonical tail up to ~14968; no ancient freezer)
Reproduction
XDC-v270 --allow-builtin-config-override --set-head 14835 \
--datadir <DIR> --networkid 5151 --syncmode full --gcmode full ...
Observed
INFO Rewound blockchain to past state number=14836 hash=366c16..6a00cf
INFO Rewound blockchain to past state number=14835 hash=3a1f83..ab56bc
INFO Rewound blockchain to past state number=14834 hash=bdafae..351abb
Fatal: Failed to register the Ethereum service: can't rollback to 14835 which is greater than current 14834
The node rewinds past the requested target (14835 → 14834), then the subsequent SetHead(14835) validation sees 14835 > currentHead(14834) and aborts.
Expected
Rewind to exactly 14835 and start normally at head 14835 (as debug.setHead("0x39f3") does on the go-ethereum-based clients).
Notes / contrast
- The same node without
--set-head loads cleanly at 14835 (Loaded most recent local block number=14835), so the underlying DB is fine — the flag's rewind loop is the problem.
debug_setHead is not exposed over RPC on this build (the method debug_setHead does not exist/is not available), so --set-head is the only rollback mechanism available to operators, which makes this bug more painful.
Likely cause
The rewind routine appears to walk down to the first block with available state (14834 here, one below the requested 14835 because 14835's state boundary condition is off-by-one), then re-validates the requested target against the new current head and errors because it went one too far. Suspect an off-by-one / >= vs > in the SetHead target validation or in the "rewind to block with state" loop.
Impact
Medium — blocks a documented recovery workflow (--set-head) and, combined with the missing debug_setHead RPC, leaves operators with no working way to drop an uncommitted tail without wiping+resyncing the datadir.
Summary
The
--set-head <N>startup flag (envXDC_SET_HEAD, "Rollback chain to block number") overshoots by one block and then fatally aborts startup whenNis at/near the node's current committed head. Instead of rewinding to N and continuing, it rewinds toN,N-1,N-2, ... and crashes with:This makes the flag unusable for its primary purpose (dropping a small uncommitted tail during recovery), and leaves the node down rather than at head N.
Environment
XDCv2.7.0-devnet (XDC-v270)Reproduction
Observed
The node rewinds past the requested target (14835 → 14834), then the subsequent SetHead(14835) validation sees
14835 > currentHead(14834)and aborts.Expected
Rewind to exactly 14835 and start normally at head 14835 (as
debug.setHead("0x39f3")does on the go-ethereum-based clients).Notes / contrast
--set-headloads cleanly at 14835 (Loaded most recent local block number=14835), so the underlying DB is fine — the flag's rewind loop is the problem.debug_setHeadis not exposed over RPC on this build (the method debug_setHead does not exist/is not available), so--set-headis the only rollback mechanism available to operators, which makes this bug more painful.Likely cause
The rewind routine appears to walk down to the first block with available state (14834 here, one below the requested 14835 because 14835's state boundary condition is off-by-one), then re-validates the requested target against the new current head and errors because it went one too far. Suspect an off-by-one /
>=vs>in the SetHead target validation or in the "rewind to block with state" loop.Impact
Medium — blocks a documented recovery workflow (
--set-head) and, combined with the missingdebug_setHeadRPC, leaves operators with no working way to drop an uncommitted tail without wiping+resyncing the datadir.