Skip to content

Commit 14a1ce5

Browse files
HristoStaykovreo101
authored andcommitted
doc(sequencer/reorg_tracking): Add documentation for reorg_tracking.rs
1 parent e82b723 commit 14a1ce5

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Reorg Tracking
2+
3+
This document describes how `apps/sequencer/src/providers/reorg_tracking.rs` monitors
4+
canonical chain progress, detects reorganizations, and recovers the Sequencer’s
5+
state.
6+
7+
## Tracker State and Inputs
8+
9+
- `ReorgTracker` keeps per-network state: the most recent finalized height,
10+
the highest height we have observed locally, an iteration counter, and the
11+
RPC timeout derived from `ReorgConfig`.
12+
- Access to providers is shared via `SharedRpcProviders`, where each
13+
`RpcProvider` owns the HTTP transport, optional websocket transport, and an
14+
`InflightObservations` cache (`observed_block_hashes` plus
15+
`non_finalized_updates`).
16+
- Reintroduced batches are sent through a `CountedSender<BatchOfUpdatesToProcess>`
17+
so the relayer loop can replay messages that were lost on a fork.
18+
- When websocket support is configured (`Provider.websocket_url` with optional
19+
`WebsocketReconnectConfig`) the tracker builds a `ResilientWsConnect` to
20+
subscribe to `eth_subscribe:newHeads`.
21+
22+
## Control Loop (`loop_tracking_for_reorg_in_network`)
23+
24+
1. **Transport setup** – If a websocket URL is present we attempt to connect
25+
and subscribe to `newHeads`. The handshake and subsequent reconnects use the
26+
`WsReconnectPolicy` backoff helpers (`should_attempt_ws`, `schedule_ws_retry`,
27+
`reset_ws_backoff`). On failure the tracker logs the reason, schedules a
28+
retry, and temporarily falls back to HTTP polling.
29+
2. **Polling cadence** – For pure HTTP operation we call
30+
`calculate_block_generation_time_in_network` (look back 100 blocks) to
31+
estimate the poll interval. All block reads are wrapped in
32+
`actix_web::rt::time::timeout`; warnings are emitted when a 5‑second deadline
33+
is exceeded.
34+
3. **Per-iteration work** – Each loop iteration:
35+
- Clones the provider handle and a snapshot of `observed_block_hashes`.
36+
- Selects the websocket provider when available (HTTP otherwise) for reads.
37+
- Fetches the latest head (`BlockNumberOrTag::Latest`) and, if successful,
38+
passes it to `process_new_block`.
39+
- Reads the on-chain ADFS root via `rpc_get_storage_at`. If the storage slot
40+
differs from the locally tracked Merkle root we update
41+
`RpcProvider.merkle_root_in_contract` and mark that the ring buffer indices
42+
must be resynchronized.
43+
- Fetches `BlockNumberOrTag::Finalized` to advance
44+
`observer_finalized_height`. When finalization moves forward we prune both
45+
`non_finalized_updates` and `observed_block_hashes` through
46+
`InflightObservations::prune_observed_up_to`. If the tracker falls behind
47+
finalization we reset `observed_latest_height` to the finalized height and
48+
seed the observed hash from the finalized block.
49+
- When `need_resync_indices` is true we call `try_to_sync` so the ring-buffer
50+
indices are refreshed directly from the contract state.
51+
- If the provider disappears from the shared map we log and terminate the
52+
loop for that network.
53+
54+
## Detecting Divergence (`process_new_block`)
55+
56+
- **Tip pre-check** – Before ingesting new blocks we refetch the chain block at
57+
`observed_latest_height`. A hash mismatch signals that the tracked tip was
58+
replaced, so we increment `ProviderMetrics.observed_reorgs` and delegate to
59+
`handle_reorg`.
60+
- **Parent mismatch** – When new blocks appear we load the first new block and
61+
compare its parent hash to the stored hash for `observed_latest_height`. Any
62+
difference indicates a fork at or above that height.
63+
- **Same-height hash change** – Even if the height does not advance, the latest
64+
header fetched from RPC is compared with the cached hash. If it changes we
65+
treat it as a reorg.
66+
- Successful ingestion stores fresh hashes in
67+
`provider.inflight.observed_block_hashes` for every block seen, ensuring we
68+
can later walk backwards to locate a common ancestor.
69+
70+
All block and storage reads funnel through `rpc_get_block_by_number` and
71+
`rpc_get_storage_at`. These helpers prefer the websocket provider when it is
72+
healthy and fall back to HTTP otherwise, still enforcing the per-call timeout.
73+
74+
## Handling a Reorg (`handle_reorg`)
75+
76+
1. Walk the cached heights in descending order (starting from the newest cached
77+
block) and refetch each block from the chain until we locate the first height
78+
where the stored hash matches the canonical hash. The function logs any
79+
diverged heights it sees along the way.
80+
2. The fork point is `first_common + 1`. We log both the ancestor and the fork
81+
height for operators.
82+
3. Holding the provider lock, we remove every entry in
83+
`non_finalized_updates` with a height ≥ fork height. Each batch is sent back
84+
to the relayer channel in ascending order and the resend outcome is logged.
85+
Pre-fork entries stay untouched so they can be
86+
pruned only when the network finalizes them.
87+
4. If no common ancestor is found within the cached history we warn, but the
88+
loop continues on the canonical head revealed by RPC.
89+
90+
## Finalization and Cleanup
91+
92+
- `observer_finalized_height` tracks the latest finalized block we have seen.
93+
Advancing it triggers a pruning pass in `InflightObservations` which clears
94+
outdated block hashes and relayer batches that can never reorg again.
95+
- If we discover that `observed_latest_height` lags behind finalization we
96+
fast-forward it to the finalized height and overwrite the cached hash to keep
97+
the tracker anchored to known-final data.
98+
- Additional blocks observed after a fork are appended to
99+
`observed_block_hashes`, giving the tracker the history it needs for future
100+
reorg detection.
101+
102+
## Websocket Strategy and Fallbacks
103+
104+
- When websockets are configured the tracker continuously consumes the
105+
`newHeads` stream. Every received header simply wakes the loop so the same
106+
verification logic runs against HTTP (to reuse existing RPC primitives).
107+
- Disconnects or subscription failures trigger a configurable backoff sequence.
108+
While waiting for the next retry we revert to the polling cadence determined
109+
by the average block time.
110+
- If no websocket URL is configured we never try to connect; the loop purely
111+
relies on the adaptive polling interval.
112+
113+
## Metrics and Observability
114+
115+
- `ProviderMetrics.observed_reorgs` (an `IntCounterVec` keyed by network) is the
116+
primary signal that a reorg was detected. Every detection increments it before
117+
the corrective flow begins.
118+
- Logs include network, observed/latest heights, finalized checkpoints, and
119+
loop counters, mirroring the legacy `eth_send_utils` diagnostics so existing
120+
dashboards remain useful. Diverged block hashes, fork points, and resend
121+
activity are explicitly printed.
122+
123+
## Test Coverage
124+
125+
- `test_loop_tracking_reorg_detect_and_resync_indices_http` and
126+
`test_loop_tracking_reorg_detect_and_resync_indices_websock`
127+
(`apps/sequencer/src/providers/reorg_tracking.rs:1573`) spin up an Anvil node,
128+
inject synthetic non-finalized batches (tagged via `EncodedFeedId`), and drive
129+
a deterministic reorg using snapshot/revert.
130+
- The tests assert that:
131+
- `observed_reorgs` increments for the network.
132+
- Only the batches at or above the fork height are replayed through the
133+
relayer channel and they arrive in ascending order.
134+
- Pre-fork batches remain cached until later finalization causes pruning.
135+
- The on-chain ADFS root drift triggers a `try_to_sync` resync, even without
136+
deploying contracts.
137+
- Both the HTTP polling path and websocket-triggered path exercise the same
138+
logic by running the scenario twice.
139+
140+
Together these pieces ensure the Sequencer can withstand short-lived forks,
141+
recover the state required to keep publishing updates, and provide operators the
142+
signals they need to observe the system’s behavior.

0 commit comments

Comments
 (0)