@aztec/sequencer-client 3.0.0-nightly.20251224 → 3.0.0-nightly.20251225
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/sequencer/checkpoint_proposal_job.d.ts +7 -6
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +14 -12
- package/dest/sequencer/metrics.d.ts +3 -1
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +9 -0
- package/dest/sequencer/sequencer.d.ts +5 -5
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +2 -2
- package/dest/sequencer/timetable.d.ts +36 -16
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +111 -56
- package/dest/sequencer/utils.d.ts +3 -3
- package/dest/sequencer/utils.js +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +83 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +179 -0
- package/dest/test/utils.d.ts +49 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +94 -0
- package/package.json +27 -27
- package/src/sequencer/README.md +531 -0
- package/src/sequencer/checkpoint_proposal_job.ts +21 -19
- package/src/sequencer/metrics.ts +11 -0
- package/src/sequencer/sequencer.ts +5 -5
- package/src/sequencer/timetable.ts +135 -76
- package/src/sequencer/utils.ts +3 -3
- package/src/test/mock_checkpoint_builder.ts +247 -0
- package/src/test/utils.ts +137 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
# Sequencer Timing Model
|
|
2
|
+
|
|
3
|
+
The Aztec sequencer divides each slot into **fixed-duration sub-slots**. Each sub-slot has a pre-defined start and end time based on an initialization offset (how much time we expect syncing the previous slot will take), a finalization time (how much time we need for closing a checkpoint and publishing it to L1), and the configured block duration.
|
|
4
|
+
|
|
5
|
+
**Example: 72-second slot with 8-second sub-slots**
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
0s: Slot starts
|
|
9
|
+
0-2s: Sync + proposer check (fixed 2s offset)
|
|
10
|
+
|
|
11
|
+
Sub-slot 1: 2s-10s → Build Block 1, deadline at 10s
|
|
12
|
+
Sub-slot 2: 10s-18s → Build Block 2, deadline at 18s
|
|
13
|
+
Sub-slot 3: 18s-26s → Build Block 3, deadline at 26s
|
|
14
|
+
Sub-slot 4: 26s-34s → Build Block 4, deadline at 34s
|
|
15
|
+
Sub-slot 5: 34s-42s → Build Block 5 (last block), deadline at 42s
|
|
16
|
+
Sub-slot 6: 42s-50s → Reserved for validators to re-execute Block 5
|
|
17
|
+
|
|
18
|
+
42s: Broadcast checkpoint with Block 5
|
|
19
|
+
44s: Validators receive proposal (2s propagation)
|
|
20
|
+
44-52s: Validators re-execute Block 5 (8s)
|
|
21
|
+
52s: Validators send attestations
|
|
22
|
+
54s: Proposer receives attestations (2s propagation)
|
|
23
|
+
54-55s: Finalize checkpoint (1s)
|
|
24
|
+
55-67s: Publish to L1 (12s)
|
|
25
|
+
72s: Slot ends
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Deadlines are fixed relative to slot start, not relative to when work actually completes. If you finish initialization at 1s instead of 2s, you get bonus time for Block 1. If you finish at 3s, you have less time. If you finish at 9s, you skip the first sub-slot altogether and start building for the second one.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
|
|
34
|
+
The Aztec sequencer operates in fixed-duration **slots** (typically 72 seconds). During each slot, a designated proposer builds multiple **blocks** containing transactions over multiple **sub-slots**, then collects a single round of attestations for the entire **checkpoint** from validators, and finally publishes the resulting checkpoint to L1 Ethereum.
|
|
35
|
+
|
|
36
|
+
## Key Concepts
|
|
37
|
+
|
|
38
|
+
### Slot vs Block vs Checkpoint vs Sub-Slot
|
|
39
|
+
|
|
40
|
+
- **Slot**: A fixed time window (e.g., 72 seconds) during which a proposer can build blocks
|
|
41
|
+
- **Block**: A single batch of transactions, executed and validated
|
|
42
|
+
- **Checkpoint**: The collection of all blocks built in a slot, attested by validators and published to L1
|
|
43
|
+
- **Sub-slot**: A fixed-duration time window within a slot (e.g., 8 seconds) during which a block should be built
|
|
44
|
+
|
|
45
|
+
In a typical configuration, a 72-second slot contains:
|
|
46
|
+
- 1 initialization period (2 seconds)
|
|
47
|
+
- 5 block-building sub-slots (8 seconds each = 40 seconds)
|
|
48
|
+
- 1 last validator re-execution sub-slot (8 seconds)
|
|
49
|
+
- 1 attestation and publishing period (17 seconds)
|
|
50
|
+
|
|
51
|
+
### The Fixed Sub-Slot Model
|
|
52
|
+
|
|
53
|
+
Building multiple blocks per slot uses **fixed sub-slots** with predictable deadlines:
|
|
54
|
+
|
|
55
|
+
1. **Equal-duration sub-slots**: All sub-slots have the same duration (`BLOCK_DURATION`)
|
|
56
|
+
2. **Fixed deadlines**: Block N deadline = `initializationOffset + N * BLOCK_DURATION`
|
|
57
|
+
3. **Last sub-slot reserved**: The final sub-slot is reserved for validators to re-execute the last block (no block is built during this sub-slot)
|
|
58
|
+
4. **Skip if too late**: If we can't start a block with at least `MIN_EXECUTION_TIME` remaining before its deadline, we immediately start building in the next sub-slot
|
|
59
|
+
|
|
60
|
+
## Timing Components
|
|
61
|
+
|
|
62
|
+
Understanding slot timing requires knowing these time constants:
|
|
63
|
+
|
|
64
|
+
| Component | Example Value | Purpose |
|
|
65
|
+
|-----------|---------------|---------|
|
|
66
|
+
| **Slot Duration** | 72s | Total time available for the entire checkpoint |
|
|
67
|
+
| **Block Duration** | 8s | Duration of each sub-slot (time budget for building one block) |
|
|
68
|
+
| **Initialization Offset** | 2s | Fixed estimate for sync + proposer check |
|
|
69
|
+
| **Propagation Time** | 2s | Time for messages to travel across the P2P network (one-way) |
|
|
70
|
+
| **Finalization Time** | 1s | Time to finalize checkpoint and prepare proposal message |
|
|
71
|
+
| **L1 Publishing Time** | 12s | Time reserved for L1 transaction to land in an Ethereum block |
|
|
72
|
+
| **Min Execution Time** | 3s | Minimum time needed to meaningfully build a block |
|
|
73
|
+
|
|
74
|
+
These values are configurable but must satisfy certain constraints (explained below). Example values may differ from the ones in the source code.
|
|
75
|
+
|
|
76
|
+
## Calculating Sub-Slots and Blocks
|
|
77
|
+
|
|
78
|
+
Given a slot configuration, we calculate how many blocks fit using this formula:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
timeReservedAtEnd = blockDuration (last sub-slot for reexecution)
|
|
82
|
+
+ propagationTime (validators receive proposal)
|
|
83
|
+
+ propagationTime (attestations come back)
|
|
84
|
+
+ finalizationTime (checkpoint finalization)
|
|
85
|
+
+ l1PublishingTime (L1 transaction)
|
|
86
|
+
|
|
87
|
+
timeAvailableForBlocks = slotDuration - initializationOffset - timeReservedAtEnd
|
|
88
|
+
|
|
89
|
+
numberOfBlocks = floor(timeAvailableForBlocks / blockDuration)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Example with typical values:**
|
|
93
|
+
```
|
|
94
|
+
timeReservedAtEnd = 8s + 2s + 2s + 1s + 12s = 25s
|
|
95
|
+
timeAvailableForBlocks = 72s - 2s - 25s = 45s
|
|
96
|
+
numberOfBlocks = floor(45s / 8s) = 5 blocks
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This means:
|
|
100
|
+
- Sub-slots 1-5: Build blocks 1-5
|
|
101
|
+
- Sub-slot 6: Reserved for validator re-execution of block 5
|
|
102
|
+
- After sub-slot 6: Attestation collection, finalization, and L1 publishing
|
|
103
|
+
|
|
104
|
+
## The Sequencer's Work
|
|
105
|
+
|
|
106
|
+
When elected as proposer for a slot, the sequencer performs these tasks:
|
|
107
|
+
|
|
108
|
+
### 1. Initialization Phase
|
|
109
|
+
|
|
110
|
+
Before building any blocks, the sequencer must:
|
|
111
|
+
- Verify it's the designated proposer
|
|
112
|
+
- Check all subsystems are synced
|
|
113
|
+
- Initialize checkpoint state
|
|
114
|
+
- Prepare global variables
|
|
115
|
+
|
|
116
|
+
Note that the initialization phase has a **fixed time budget** (`initializationOffset`, typically 2s). This is an *estimate*, not a deadline. The sequencer will take as long as it needs for initialization, but the sub-slot deadlines remain fixed regardless.
|
|
117
|
+
|
|
118
|
+
### 2. Block Building Loop
|
|
119
|
+
|
|
120
|
+
The sequencer builds blocks in **fixed sub-slots** based on the configured block duration.
|
|
121
|
+
|
|
122
|
+
#### Sub-slot deadline calculation
|
|
123
|
+
|
|
124
|
+
Each sub-slot has a fixed start time and deadline:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
subSlotStart[N] = initializationOffset + (N - 1) * blockDuration
|
|
128
|
+
subSlotDeadline[N] = initializationOffset + N * blockDuration
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Where N is the sub-slot number (1-indexed).
|
|
132
|
+
|
|
133
|
+
**Example with 2s offset and 8s block duration:**
|
|
134
|
+
```
|
|
135
|
+
Sub-slot 1: starts at 2s, deadline at 10s
|
|
136
|
+
Sub-slot 2: starts at 10s, deadline at 18s
|
|
137
|
+
Sub-slot 3: starts at 18s, deadline at 26s
|
|
138
|
+
Sub-slot 4: starts at 26s, deadline at 34s
|
|
139
|
+
Sub-slot 5: starts at 34s, deadline at 42s
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Building a block
|
|
143
|
+
|
|
144
|
+
For each sub-slot, the sequencer:
|
|
145
|
+
|
|
146
|
+
1. **Checks if we can start**: If current time is past `deadline - MIN_EXECUTION_TIME`, skip this sub-slot, and start building the block as if it were on the next sub-slot
|
|
147
|
+
2. **Waits for transactions** (if needed): Wait up to the deadline for minimum number of transactions
|
|
148
|
+
3. **Builds block**: Execute transactions until the deadline of the sub-slot
|
|
149
|
+
4. **Signs and broadcasts**: Finalize block and broadcast proposal to validators
|
|
150
|
+
|
|
151
|
+
**Key point:** The deadline is **fixed** based on the sub-slot number, not based on when the previous block finished.
|
|
152
|
+
|
|
153
|
+
Note that if a block finishes early, then the sequencer waits until the next sub-slot starts to maintain the regular interval. This prevents "rushing ahead" and keeps the timing predictable. Conversely, if the block finishes later than expected, this "eats into" the time budget for the next block.
|
|
154
|
+
|
|
155
|
+
#### Waiting for minimum transactions
|
|
156
|
+
|
|
157
|
+
Before building a block, the sequencer must ensure there are enough transactions in the mempool. This waiting phase has its own timing constraints:
|
|
158
|
+
|
|
159
|
+
**Configuration:**
|
|
160
|
+
- `minTxsPerBlock`: Minimum number of transactions required (configurable, e.g., 1-4)
|
|
161
|
+
- Polling interval: 500ms (checks mempool every half-second)
|
|
162
|
+
- Deadline: `blockDeadline - 1000ms` (must start building at least 1 second before the block deadline)
|
|
163
|
+
|
|
164
|
+
**Behavior:**
|
|
165
|
+
1. Check current pending transaction count
|
|
166
|
+
2. If count >= `minTxsPerBlock`, proceed to build immediately
|
|
167
|
+
3. If count < `minTxsPerBlock`, poll every 500ms until either:
|
|
168
|
+
- Enough transactions arrive (proceed to build)
|
|
169
|
+
- Deadline is reached (skip building this block)
|
|
170
|
+
|
|
171
|
+
**Special cases:**
|
|
172
|
+
- **Last block with empty checkpoint allowed**: If `buildCheckpointIfEmpty` is true and this is the last block, skip waiting and force build with 0+ transactions
|
|
173
|
+
- **Non-enforced timetable**: If enforcement is disabled, exit immediately if not enough transactions (don't wait)
|
|
174
|
+
|
|
175
|
+
**Example:**
|
|
176
|
+
```
|
|
177
|
+
Sub-slot 3 deadline: 26s
|
|
178
|
+
Transaction wait deadline: 25s (26s - 1s)
|
|
179
|
+
Current time: 20s
|
|
180
|
+
|
|
181
|
+
20.0s: Check mempool → 2 txs (need 4)
|
|
182
|
+
20.5s: Check mempool → 2 txs (need 4)
|
|
183
|
+
21.0s: Check mempool → 4 txs (need 4) ✓ Start building!
|
|
184
|
+
21-26s: Build block with those 4+ transactions
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
If the deadline (25s) is reached with only 2 transactions, the block is skipped and the sequencer moves to the next sub-slot.
|
|
188
|
+
|
|
189
|
+
### 3. Last Block and Validator Re-execution
|
|
190
|
+
|
|
191
|
+
The **last block** is built during the **penultimate sub-slot**. The final sub-slot is reserved for validators to re-execute the last block.
|
|
192
|
+
|
|
193
|
+
**Why the last sub-slot is reserved:**
|
|
194
|
+
|
|
195
|
+
Validators execute blocks **sequentially**. While the proposer builds Block N+1, validators are re-executing Block N (with a ~2s delay due to propagation). However, for the **last block**, there's no "Block N+1" to build while validators re-execute. We must wait for them to finish so they can attest.
|
|
196
|
+
|
|
197
|
+
**Timeline for the last block:**
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
T: Last block finishes building, checkpoint proposal broadcast
|
|
201
|
+
Last sub-slot begins (duration: blockDuration)
|
|
202
|
+
T+2s: Validators receive proposal (propagation delay)
|
|
203
|
+
T+2s to T+2s+blockDuration: Validators re-execute last block
|
|
204
|
+
T+2s+blockDuration: Validators finish re-execution, send attestations
|
|
205
|
+
T+4s+blockDuration: Proposer receives attestations (propagation delay)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Example with 8s block duration:**
|
|
209
|
+
```
|
|
210
|
+
42s: Block 5 finishes, checkpoint broadcast, sub-slot 6 starts
|
|
211
|
+
44s: Validators receive checkpoint (42s + 2s)
|
|
212
|
+
44-52s: Validators re-execute Block 5 (8s)
|
|
213
|
+
52s: Validators send attestations
|
|
214
|
+
54s: Proposer receives attestations (52s + 2s)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Note that validators finish at `52s`, which is `2s` after the last sub-slot ends at `50s`. This is expected and accounted for in the `timeReservedAtEnd` calculation.
|
|
218
|
+
|
|
219
|
+
### 4. Attestation Collection and L1 Publishing
|
|
220
|
+
|
|
221
|
+
After the last block is built and validators have re-executed it:
|
|
222
|
+
|
|
223
|
+
1. **Collect attestations**: Wait for validators to send their signatures (arrive at T+4s+blockDuration)
|
|
224
|
+
2. **Finalize checkpoint**: Sign over attestations, assemble final checkpoint (1s)
|
|
225
|
+
3. **Publish to L1**: Submit transaction to Ethereum (needs 12s to land)
|
|
226
|
+
|
|
227
|
+
**Time reserved:** `2*propagationTime + finalizationTime + l1PublishingTime = 2s + 2s + 1s + 12s = 17s`
|
|
228
|
+
|
|
229
|
+
This 17s comes after the last sub-slot, ensuring we have enough time to complete the checkpoint. If the sequencer receives the necessary attestations before the reserved time, the L1 tx is submitted earlier.
|
|
230
|
+
|
|
231
|
+
## Handling Timing Variations
|
|
232
|
+
|
|
233
|
+
How does the sequencer timetable handle deviations from the expected times.
|
|
234
|
+
|
|
235
|
+
### Fast Initialization
|
|
236
|
+
|
|
237
|
+
**Scenario:** Initialization completes at 1s instead of 2s
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
0-1s: SYNCHRONIZING, PROPOSER_CHECK (1s actual, vs 2s estimate)
|
|
241
|
+
1s: Ready to build Block 1
|
|
242
|
+
1-10s: Build Block 1 (9s available vs 8s budgeted)
|
|
243
|
+
10s: Block 1 deadline
|
|
244
|
+
10-18s: Build Block 2
|
|
245
|
+
...
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Result:** Block 1 gets a bonus 1s of execution time. The extra time allows for more transactions or more complex execution.
|
|
249
|
+
|
|
250
|
+
### Slow Initialization
|
|
251
|
+
|
|
252
|
+
**Scenario:** Initialization completes at 3s instead of 2s
|
|
253
|
+
|
|
254
|
+
This may happen if the sequencer has a slow L1 RPC endpoint and syncing the previous checkpoint from L1 takes longer than expected.
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
0-3s: SYNCHRONIZING, PROPOSER_CHECK (3s actual, vs 2s estimate)
|
|
258
|
+
3s: Ready to build Block 1
|
|
259
|
+
3-10s: Build Block 1 (7s available vs 8s budgeted)
|
|
260
|
+
10s: Block 1 deadline
|
|
261
|
+
10-18s: Build Block 2
|
|
262
|
+
...
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Result:** Block 1 has 1s less time (7s instead of 8s). Still enough time to build a block, just with fewer transactions or simpler execution.
|
|
266
|
+
|
|
267
|
+
### Very Slow Initialization
|
|
268
|
+
|
|
269
|
+
**Scenario:** Initialization completes at 9s instead of 2s
|
|
270
|
+
|
|
271
|
+
While extremely unlikely, we still account for this scenario. We'd expect it to be related to faults to syncing blob data.
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
0-9s: SYNCHRONIZING, PROPOSER_CHECK (9s actual, vs 2s estimate)
|
|
275
|
+
9s: Ready to build Block 1
|
|
276
|
+
Check: Can we start Block 1 in sub-slot 1?
|
|
277
|
+
- Sub-slot 1 deadline: 10s
|
|
278
|
+
- Current time: 9s
|
|
279
|
+
- Time available: 1s
|
|
280
|
+
- MIN_EXECUTION_TIME: 3s
|
|
281
|
+
- 1s < 3s, so CANNOT use sub-slot 1
|
|
282
|
+
|
|
283
|
+
Use sub-slot 2 instead:
|
|
284
|
+
9s: Start building Block 1 using sub-slot 2
|
|
285
|
+
9-18s: Build Block 1 (9s available vs 8s budgeted)
|
|
286
|
+
18s: Block 1 deadline (sub-slot 2)
|
|
287
|
+
18-26s: Build Block 2 using sub-slot 3
|
|
288
|
+
...
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Result:** Sub-slot 1 is skipped entirely. We build 4 blocks instead of 5 (using sub-slots 2-5). Block 1 gets a bonus 1s of time.
|
|
292
|
+
|
|
293
|
+
### Block Takes Longer Than Expected
|
|
294
|
+
|
|
295
|
+
**Scenario:** Block 2 takes 9s instead of 8s
|
|
296
|
+
|
|
297
|
+
This scenario should not happen since the sequencer forcefully stops the block builder at the given deadline, but we still consider it.
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
10s: Start building Block 2
|
|
301
|
+
19s: Block 2 finishes (1s late, deadline was 18s)
|
|
302
|
+
19s: Broadcast Block 2
|
|
303
|
+
Check: Can we start Block 3?
|
|
304
|
+
- Block 3 deadline: 26s
|
|
305
|
+
- Current time: 19s
|
|
306
|
+
- Time available: 7s
|
|
307
|
+
- MIN_EXECUTION_TIME: 3s
|
|
308
|
+
- 7s >= 3s, so CAN start Block 3
|
|
309
|
+
|
|
310
|
+
19-26s: Build Block 3 (7s available vs 8s budgeted)
|
|
311
|
+
26s: Block 3 deadline
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Result:** Block 3 has less time (7s instead of 8s), but we still build it. The delay propagates but doesn't cascade uncontrollably.
|
|
315
|
+
|
|
316
|
+
**Extreme case:** If Block 2 finishes at 24s (6s late):
|
|
317
|
+
```
|
|
318
|
+
24s: Block 2 finishes (6s late)
|
|
319
|
+
Check: Can we start Block 3 in sub-slot 3?
|
|
320
|
+
- Sub-slot 3 deadline: 26s
|
|
321
|
+
- Current time: 24s
|
|
322
|
+
- Time available: 2s
|
|
323
|
+
- MIN_EXECUTION_TIME: 3s
|
|
324
|
+
- 2s < 3s, so CANNOT use sub-slot 3
|
|
325
|
+
|
|
326
|
+
Use sub-slot 4 instead:
|
|
327
|
+
24-34s: Build Block 3 using sub-slot 4 (10s available vs 8s budgeted)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Result:** Sub-slot 3 is skipped, we build Block 3 using sub-slot 4 instead with bonus time.
|
|
331
|
+
|
|
332
|
+
### Block Finishes Early
|
|
333
|
+
|
|
334
|
+
**Scenario:** Block 2 finishes at 15s instead of 18s
|
|
335
|
+
|
|
336
|
+
This can happen if the sequencer hits a block limit (number of txs, gas, size, etc) or runs out of available txs before the sub-slot deadline:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
10-15s: Build Block 2 (5s used vs 8s budgeted)
|
|
340
|
+
15s: Block 2 finished
|
|
341
|
+
15s: Broadcast Block 2
|
|
342
|
+
Check: Should we start Block 3 now or wait?
|
|
343
|
+
- Next sub-slot starts at 18s
|
|
344
|
+
- Wait until 18s to maintain regular intervals
|
|
345
|
+
|
|
346
|
+
15-18s: WAITING_UNTIL_NEXT_BLOCK
|
|
347
|
+
18s: Start building Block 3
|
|
348
|
+
18-26s: Build Block 3
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Result:** We wait until the next sub-slot starts. This prevents "rushing ahead" and maintains consistent block intervals, which is better for validators who are re-executing blocks in parallel.
|
|
352
|
+
|
|
353
|
+
## Parallel execution between Proposers and Validators
|
|
354
|
+
|
|
355
|
+
A key aspect of this design is **parallel execution** between proposer and validators.
|
|
356
|
+
|
|
357
|
+
### Timeline Example (8-second sub-slots)
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
Time | Proposer | Validators
|
|
361
|
+
-----|----------------------------|---------------------------
|
|
362
|
+
2s | Start building Block 1 | (idle)
|
|
363
|
+
10s | Finish Block 1, broadcast | (idle)
|
|
364
|
+
10s | Start building Block 2 |
|
|
365
|
+
12s | | Receive Block 1 (10s + 2s)
|
|
366
|
+
| | Start re-executing Block 1
|
|
367
|
+
18s | Finish Block 2, broadcast |
|
|
368
|
+
20s | | Finish re-executing Block 1 (12s + 8s)
|
|
369
|
+
| | Receive Block 2 (18s + 2s)
|
|
370
|
+
| | Start re-executing Block 2
|
|
371
|
+
18s | Start building Block 3 |
|
|
372
|
+
26s | Finish Block 3, broadcast |
|
|
373
|
+
28s | | Finish re-executing Block 2 (20s + 8s)
|
|
374
|
+
| | Receive Block 3 (26s + 2s)
|
|
375
|
+
| | Start re-executing Block 3
|
|
376
|
+
...
|
|
377
|
+
42s | Finish Block 5, broadcast |
|
|
378
|
+
| checkpoint proposal |
|
|
379
|
+
44s | | Finish re-executing Block 4 (36s + 8s)
|
|
380
|
+
| | Receive Block 5 + checkpoint (42s + 2s)
|
|
381
|
+
| | Start re-executing Block 5
|
|
382
|
+
42-54s| COLLECTING_ATTESTATIONS |
|
|
383
|
+
52s | | Finish re-executing Block 5 (44s + 8s)
|
|
384
|
+
| | Send attestations
|
|
385
|
+
54s | Receive attestations | (done)
|
|
386
|
+
54-55s| ASSEMBLING_CHECKPOINT |
|
|
387
|
+
55s | PUBLISHING_CHECKPOINT |
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Key observations:**
|
|
391
|
+
- Validators lag by ~2s (propagation delay)
|
|
392
|
+
- While proposer builds Block N+1, validators re-execute Block N (parallel work)
|
|
393
|
+
- For the last block, proposer waits while validators re-execute
|
|
394
|
+
- The last sub-slot provides the time budget for this waiting period
|
|
395
|
+
|
|
396
|
+
## Configuration Guidelines
|
|
397
|
+
|
|
398
|
+
When configuring timing parameters, ensure these constraints are satisfied:
|
|
399
|
+
|
|
400
|
+
### Minimum Slot Duration
|
|
401
|
+
|
|
402
|
+
For a valid configuration:
|
|
403
|
+
```
|
|
404
|
+
slotDuration >= initializationOffset
|
|
405
|
+
+ blockDuration * 2 (at least 2 blocks)
|
|
406
|
+
+ blockDuration (last sub-slot)
|
|
407
|
+
+ 2 * propagationTime (round-trip)
|
|
408
|
+
+ finalizationTime (checkpoint finalization)
|
|
409
|
+
+ l1PublishingTime (L1 publishing)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Simplified:
|
|
413
|
+
```
|
|
414
|
+
slotDuration >= initializationOffset + 3*blockDuration + 2*propagationTime + finalizationTime + l1PublishingTime
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Example:**
|
|
418
|
+
```
|
|
419
|
+
slotDuration >= 2s + 3*8s + 2*2s + 1s + 12s = 2s + 24s + 4s + 1s + 12s = 43s
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
For a 72s slot, this leaves `72s - 43s = 29s` of slack, allowing for about 3-4 additional blocks (29s / 8s ≈ 3.6).
|
|
423
|
+
|
|
424
|
+
### Block Duration Constraints
|
|
425
|
+
|
|
426
|
+
Block duration should be greater than the min execution time, and ideally a divisor of the time available for building.
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
blockDuration >= MIN_EXECUTION_TIME (3s practical minimum for meaningful execution)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Initialization Offset
|
|
433
|
+
|
|
434
|
+
The initialization offset should be set based on empirical measurements of how long initialization typically takes, with typical values being 1-3 seconds.
|
|
435
|
+
|
|
436
|
+
**Key point:** This is an *estimate*, not a hard deadline. The sequencer will take as long as needed for initialization. If it takes longer than the offset, the first block just has less time. If it takes less, the first block has bonus time.
|
|
437
|
+
|
|
438
|
+
### Propagation Time
|
|
439
|
+
|
|
440
|
+
Should be measured empirically on the actual P2P network, accounting for:
|
|
441
|
+
- Network latency between geographically distributed validators
|
|
442
|
+
- Gossip network propagation (not direct communication)
|
|
443
|
+
- Block/checkpoint size (larger messages take longer)
|
|
444
|
+
|
|
445
|
+
Typical values: 1-3 seconds
|
|
446
|
+
|
|
447
|
+
### L1 Publishing Time
|
|
448
|
+
|
|
449
|
+
Must account for Ethereum slot duration (12s) and blob propagation time:
|
|
450
|
+
- Bare minimum: 8s (Ethereum allows txs up to 4s into the slot)
|
|
451
|
+
- Recommended minimum: 12s (full Ethereum slot)
|
|
452
|
+
- With high blob congestion: 24s (two slots)
|
|
453
|
+
|
|
454
|
+
## State Machine
|
|
455
|
+
|
|
456
|
+
The sequencer transitions through these states during a slot:
|
|
457
|
+
|
|
458
|
+
| State | Time Budget | Purpose |
|
|
459
|
+
|-------|-------------|---------|
|
|
460
|
+
| **SYNCHRONIZING** | No limit | Wait for all subsystems to sync |
|
|
461
|
+
| **PROPOSER_CHECK** | Part of init offset | Verify we're the proposer |
|
|
462
|
+
| **INITIALIZING_CHECKPOINT** | Part of init offset | Set up checkpoint state |
|
|
463
|
+
| **WAITING_FOR_TXS** | Until block deadline | Wait for enough transactions |
|
|
464
|
+
| **CREATING_BLOCK** | Until block deadline | Execute transactions and build block |
|
|
465
|
+
| **WAITING_UNTIL_NEXT_BLOCK** | Until next sub-slot start | Sleep between blocks to maintain intervals |
|
|
466
|
+
| **ASSEMBLING_CHECKPOINT** | assembleTime (1s) | Assemble final checkpoint |
|
|
467
|
+
| **COLLECTING_ATTESTATIONS** | Until L1 publish deadline | Wait for validator signatures |
|
|
468
|
+
| **PUBLISHING_CHECKPOINT** | Until slot end | Submit to L1 |
|
|
469
|
+
|
|
470
|
+
## Complete Example: 72-Second Slot with 8-Second Sub-Slots
|
|
471
|
+
|
|
472
|
+
Let's walk through a complete slot with the happy path:
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
T=0s Slot begins for slot N
|
|
476
|
+
|
|
477
|
+
T=0-2s SYNCHRONIZING, PROPOSER_CHECK, INITIALIZING_CHECKPOINT
|
|
478
|
+
Actual time: 1.8s (slightly faster than 2s estimate)
|
|
479
|
+
|
|
480
|
+
T=2s Sub-slot 1 deadline calculation: 2s + 1*8s = 10s
|
|
481
|
+
T=1.8s Ready to build, start Block 1 immediately
|
|
482
|
+
Available time: 10s - 1.8s = 8.2s (bonus 0.2s!)
|
|
483
|
+
T=1.8-9.5s CREATING_BLOCK 1
|
|
484
|
+
T=9.5s Block 1 complete, broadcast
|
|
485
|
+
T=9.5-10s Wait for next sub-slot
|
|
486
|
+
|
|
487
|
+
T=10s Sub-slot 2 starts, deadline: 2s + 2*8s = 18s
|
|
488
|
+
T=10-17.5s CREATING_BLOCK 2
|
|
489
|
+
T=17.5s Block 2 complete, broadcast
|
|
490
|
+
T=17.5-18s Wait for next sub-slot
|
|
491
|
+
|
|
492
|
+
T=18s Sub-slot 3 starts, deadline: 2s + 3*8s = 26s
|
|
493
|
+
T=18-25s CREATING_BLOCK 3
|
|
494
|
+
T=25s Block 3 complete, broadcast
|
|
495
|
+
T=25-26s Wait for next sub-slot
|
|
496
|
+
|
|
497
|
+
T=26s Sub-slot 4 starts, deadline: 2s + 4*8s = 34s
|
|
498
|
+
T=26-33.5s CREATING_BLOCK 4
|
|
499
|
+
T=33.5s Block 4 complete, broadcast
|
|
500
|
+
T=33.5-34s Wait for next sub-slot
|
|
501
|
+
|
|
502
|
+
T=34s Sub-slot 5 starts, deadline: 2s + 5*8s = 42s
|
|
503
|
+
T=34-41s CREATING_BLOCK 5 (last block)
|
|
504
|
+
T=41s Block 5 complete
|
|
505
|
+
|
|
506
|
+
T=41s ASSEMBLING_CHECKPOINT (1s)
|
|
507
|
+
T=42s Checkpoint proposal broadcast
|
|
508
|
+
Sub-slot 6 starts (last sub-slot, reserved for validator reexec)
|
|
509
|
+
|
|
510
|
+
T=44s Validators receive checkpoint (42s + 2s propagation)
|
|
511
|
+
Validators start re-executing Block 5
|
|
512
|
+
|
|
513
|
+
T=52s Validators finish re-executing Block 5 (44s + 8s)
|
|
514
|
+
Validators send attestations
|
|
515
|
+
|
|
516
|
+
T=54s COLLECTING_ATTESTATIONS
|
|
517
|
+
Proposer receives attestations (52s + 2s propagation)
|
|
518
|
+
|
|
519
|
+
T=55s PUBLISHING_CHECKPOINT
|
|
520
|
+
Sign over attestations, submit L1 transaction
|
|
521
|
+
|
|
522
|
+
T=67s L1 transaction lands in Ethereum block (12s)
|
|
523
|
+
|
|
524
|
+
T=72s Slot ends (5s buffer remaining)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Summary:**
|
|
528
|
+
- Built 5 blocks (sub-slots 1-5)
|
|
529
|
+
- Last sub-slot (6) reserved for validator re-execution
|
|
530
|
+
- Total time: 72s
|
|
531
|
+
- Buffer: 5s (72s - 67s)
|
|
@@ -48,9 +48,6 @@ import { SequencerState } from './utils.js';
|
|
|
48
48
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
49
49
|
const TXS_POLLING_MS = 500;
|
|
50
50
|
|
|
51
|
-
/** What's the latest time before a block build deadline we're willing to *start* building it */
|
|
52
|
-
const MIN_BLOCK_BUILD_TIME_MS = 1000;
|
|
53
|
-
|
|
54
51
|
/**
|
|
55
52
|
* Handles the execution of a checkpoint proposal after the initial preparation phase.
|
|
56
53
|
* This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
|
|
@@ -74,15 +71,15 @@ export class CheckpointProposalJob {
|
|
|
74
71
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
75
72
|
private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
76
73
|
private readonly l1Constants: SequencerRollupConstants,
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
protected config: ResolvedSequencerConfig,
|
|
75
|
+
protected timetable: SequencerTimetable,
|
|
79
76
|
private readonly slasherClient: SlasherClientInterface | undefined,
|
|
80
77
|
private readonly epochCache: EpochCache,
|
|
81
78
|
private readonly dateProvider: DateProvider,
|
|
82
79
|
private readonly metrics: SequencerMetrics,
|
|
83
80
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
84
81
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
85
|
-
|
|
82
|
+
protected readonly log: Logger,
|
|
86
83
|
) {}
|
|
87
84
|
|
|
88
85
|
/**
|
|
@@ -188,7 +185,7 @@ export class CheckpointProposalJob {
|
|
|
188
185
|
|
|
189
186
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
190
187
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
191
|
-
this.setStateFn(SequencerState.
|
|
188
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
192
189
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
193
190
|
|
|
194
191
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
@@ -214,10 +211,14 @@ export class CheckpointProposalJob {
|
|
|
214
211
|
this.proposer,
|
|
215
212
|
blockProposalOptions,
|
|
216
213
|
);
|
|
214
|
+
const blockProposedAt = this.dateProvider.now();
|
|
217
215
|
await this.p2pClient.broadcastProposal(proposal);
|
|
218
216
|
|
|
219
217
|
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.slot);
|
|
220
218
|
const attestations = await this.waitForAttestations(proposal);
|
|
219
|
+
const blockAttestedAt = this.dateProvider.now();
|
|
220
|
+
|
|
221
|
+
this.metrics.recordBlockAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
221
222
|
|
|
222
223
|
// Proposer must sign over the attestations before pushing them to L1
|
|
223
224
|
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
@@ -290,12 +291,15 @@ export class CheckpointProposalJob {
|
|
|
290
291
|
});
|
|
291
292
|
|
|
292
293
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
293
|
-
// If no block was produced due to not enough txs and this was the last
|
|
294
|
+
// If no block was produced due to not enough txs and this was the last subslot, exit
|
|
294
295
|
break;
|
|
295
|
-
} else if (!buildResult) {
|
|
296
|
-
// But if there is still time for more blocks, wait until the next
|
|
297
|
-
await this.
|
|
296
|
+
} else if (!buildResult && timingInfo.deadline !== undefined) {
|
|
297
|
+
// But if there is still time for more blocks, wait until the next subslot and try again
|
|
298
|
+
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
298
299
|
continue;
|
|
300
|
+
} else if (!buildResult) {
|
|
301
|
+
// Exit if there is no possibility of building more blocks
|
|
302
|
+
break;
|
|
299
303
|
} else if ('error' in buildResult) {
|
|
300
304
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
301
305
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
@@ -343,7 +347,7 @@ export class CheckpointProposalJob {
|
|
|
343
347
|
}
|
|
344
348
|
|
|
345
349
|
// Wait until the next block's start time
|
|
346
|
-
await this.
|
|
350
|
+
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
347
351
|
}
|
|
348
352
|
|
|
349
353
|
this.log.verbose(`Block building loop completed for slot ${this.slot}`, {
|
|
@@ -358,12 +362,10 @@ export class CheckpointProposalJob {
|
|
|
358
362
|
}
|
|
359
363
|
|
|
360
364
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
361
|
-
private async
|
|
365
|
+
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
362
366
|
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.slot);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.log.verbose(`Waiting until time for the next block at ${nextBlockStart}s into slot`, { slot: this.slot });
|
|
366
|
-
await this.waitUntilTimeInSlot(nextBlockStart);
|
|
367
|
+
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, { slot: this.slot });
|
|
368
|
+
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
367
369
|
}
|
|
368
370
|
|
|
369
371
|
/** Builds a single block. Called from the main block building loop. */
|
|
@@ -489,7 +491,7 @@ export class CheckpointProposalJob {
|
|
|
489
491
|
|
|
490
492
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
491
493
|
const startBuildingDeadline = buildDeadline
|
|
492
|
-
? new Date(buildDeadline.getTime() -
|
|
494
|
+
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
493
495
|
: undefined;
|
|
494
496
|
|
|
495
497
|
let availableTxs = await this.p2pClient.getPendingTxCount();
|
|
@@ -680,7 +682,7 @@ export class CheckpointProposalJob {
|
|
|
680
682
|
}
|
|
681
683
|
|
|
682
684
|
/** Waits until a specific time within the current slot */
|
|
683
|
-
|
|
685
|
+
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
684
686
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
685
687
|
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|
|
686
688
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
package/src/sequencer/metrics.ts
CHANGED
|
@@ -44,6 +44,7 @@ export class SequencerMetrics {
|
|
|
44
44
|
private blockProposalPrecheckFailed: UpDownCounter;
|
|
45
45
|
private checkpointSuccess: UpDownCounter;
|
|
46
46
|
private slashingAttempts: UpDownCounter;
|
|
47
|
+
private blockAttestationDelay: Histogram;
|
|
47
48
|
|
|
48
49
|
// Fisherman fee analysis metrics
|
|
49
50
|
private fishermanWouldBeIncluded: UpDownCounter;
|
|
@@ -91,6 +92,12 @@ export class SequencerMetrics {
|
|
|
91
92
|
},
|
|
92
93
|
);
|
|
93
94
|
|
|
95
|
+
this.blockAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_ATTESTATION_DELAY, {
|
|
96
|
+
unit: 'ms',
|
|
97
|
+
description: 'The time difference between block proposal and minimal attestation count reached,',
|
|
98
|
+
valueType: ValueType.INT,
|
|
99
|
+
});
|
|
100
|
+
|
|
94
101
|
// Init gauges and counters
|
|
95
102
|
this.blockCounter.add(0, {
|
|
96
103
|
[Attributes.STATUS]: 'failed',
|
|
@@ -257,6 +264,10 @@ export class SequencerMetrics {
|
|
|
257
264
|
this.timeToCollectAttestations.record(0);
|
|
258
265
|
}
|
|
259
266
|
|
|
267
|
+
public recordBlockAttestationDelay(duration: number) {
|
|
268
|
+
this.blockAttestationDelay.record(duration);
|
|
269
|
+
}
|
|
270
|
+
|
|
260
271
|
public recordCollectedAttestations(count: number, durationMs: number) {
|
|
261
272
|
this.collectedAttestions.record(count);
|
|
262
273
|
this.timeToCollectAttestations.record(Math.ceil(durationMs));
|