@aztec/sequencer-client 0.0.1-commit.03f7ef2 → 0.0.1-commit.04d373f

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.
Files changed (145) hide show
  1. package/README.md +282 -21
  2. package/dest/client/sequencer-client.d.ts +20 -11
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +67 -33
  5. package/dest/config.d.ts +29 -7
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +82 -37
  8. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  9. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  10. package/dest/global_variable_builder/fee_predictor.js +128 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  12. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  13. package/dest/global_variable_builder/fee_provider.js +58 -0
  14. package/dest/global_variable_builder/global_builder.d.ts +14 -16
  15. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  16. package/dest/global_variable_builder/global_builder.js +16 -50
  17. package/dest/global_variable_builder/index.d.ts +4 -2
  18. package/dest/global_variable_builder/index.d.ts.map +1 -1
  19. package/dest/global_variable_builder/index.js +2 -0
  20. package/dest/index.d.ts +2 -3
  21. package/dest/index.d.ts.map +1 -1
  22. package/dest/index.js +1 -2
  23. package/dest/publisher/config.d.ts +47 -17
  24. package/dest/publisher/config.d.ts.map +1 -1
  25. package/dest/publisher/config.js +121 -42
  26. package/dest/publisher/index.d.ts +2 -1
  27. package/dest/publisher/index.d.ts.map +1 -1
  28. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  29. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  30. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  31. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
  32. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  33. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  34. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  35. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  36. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  37. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  38. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  39. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  40. package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
  41. package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
  42. package/dest/publisher/sequencer-bundle-simulator.js +198 -0
  43. package/dest/publisher/sequencer-publisher-factory.d.ts +12 -6
  44. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  45. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  46. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  47. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  48. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  49. package/dest/publisher/sequencer-publisher.d.ts +108 -81
  50. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  51. package/dest/publisher/sequencer-publisher.js +876 -403
  52. package/dest/sequencer/automine/automine_factory.d.ts +54 -0
  53. package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
  54. package/dest/sequencer/automine/automine_factory.js +84 -0
  55. package/dest/sequencer/automine/automine_sequencer.d.ts +151 -0
  56. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
  57. package/dest/sequencer/automine/automine_sequencer.js +506 -0
  58. package/dest/sequencer/chain_state_overrides.d.ts +61 -0
  59. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  60. package/dest/sequencer/chain_state_overrides.js +98 -0
  61. package/dest/sequencer/checkpoint_proposal_job.d.ts +87 -16
  62. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  63. package/dest/sequencer/checkpoint_proposal_job.js +1331 -203
  64. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  65. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  66. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  67. package/dest/sequencer/checkpoint_voter.d.ts +3 -3
  68. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  69. package/dest/sequencer/checkpoint_voter.js +34 -13
  70. package/dest/sequencer/events.d.ts +47 -2
  71. package/dest/sequencer/events.d.ts.map +1 -1
  72. package/dest/sequencer/index.d.ts +3 -3
  73. package/dest/sequencer/index.d.ts.map +1 -1
  74. package/dest/sequencer/index.js +2 -2
  75. package/dest/sequencer/metrics.d.ts +23 -8
  76. package/dest/sequencer/metrics.d.ts.map +1 -1
  77. package/dest/sequencer/metrics.js +158 -143
  78. package/dest/sequencer/sequencer.d.ts +92 -29
  79. package/dest/sequencer/sequencer.d.ts.map +1 -1
  80. package/dest/sequencer/sequencer.js +818 -157
  81. package/dest/sequencer/timetable.d.ts +17 -6
  82. package/dest/sequencer/timetable.d.ts.map +1 -1
  83. package/dest/sequencer/timetable.js +51 -46
  84. package/dest/sequencer/types.d.ts +2 -2
  85. package/dest/sequencer/types.d.ts.map +1 -1
  86. package/dest/test/index.d.ts +4 -7
  87. package/dest/test/index.d.ts.map +1 -1
  88. package/dest/test/mock_checkpoint_builder.d.ts +29 -17
  89. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  90. package/dest/test/mock_checkpoint_builder.js +86 -34
  91. package/dest/test/utils.d.ts +13 -9
  92. package/dest/test/utils.d.ts.map +1 -1
  93. package/dest/test/utils.js +31 -20
  94. package/package.json +29 -28
  95. package/src/client/sequencer-client.ts +94 -38
  96. package/src/config.ts +107 -46
  97. package/src/global_variable_builder/README.md +44 -0
  98. package/src/global_variable_builder/fee_predictor.ts +172 -0
  99. package/src/global_variable_builder/fee_provider.ts +75 -0
  100. package/src/global_variable_builder/global_builder.ts +26 -63
  101. package/src/global_variable_builder/index.ts +3 -1
  102. package/src/index.ts +5 -4
  103. package/src/publisher/config.ts +157 -45
  104. package/src/publisher/index.ts +3 -0
  105. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  106. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  107. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  108. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  109. package/src/publisher/sequencer-bundle-simulator.ts +254 -0
  110. package/src/publisher/sequencer-publisher-factory.ts +39 -10
  111. package/src/publisher/sequencer-publisher-metrics.ts +17 -69
  112. package/src/publisher/sequencer-publisher.ts +611 -521
  113. package/src/sequencer/README.md +152 -450
  114. package/src/sequencer/automine/README.md +46 -0
  115. package/src/sequencer/automine/automine_factory.ts +145 -0
  116. package/src/sequencer/automine/automine_sequencer.ts +592 -0
  117. package/src/sequencer/chain_state_overrides.ts +162 -0
  118. package/src/sequencer/checkpoint_proposal_job.ts +1138 -230
  119. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  120. package/src/sequencer/checkpoint_voter.ts +31 -17
  121. package/src/sequencer/events.ts +50 -2
  122. package/src/sequencer/index.ts +2 -2
  123. package/src/sequencer/metrics.ts +168 -151
  124. package/src/sequencer/sequencer.ts +535 -162
  125. package/src/sequencer/timetable.ts +70 -57
  126. package/src/sequencer/types.ts +1 -1
  127. package/src/test/index.ts +3 -6
  128. package/src/test/mock_checkpoint_builder.ts +149 -73
  129. package/src/test/utils.ts +78 -30
  130. package/dest/sequencer/block_builder.d.ts +0 -26
  131. package/dest/sequencer/block_builder.d.ts.map +0 -1
  132. package/dest/sequencer/block_builder.js +0 -129
  133. package/dest/sequencer/checkpoint_builder.d.ts +0 -63
  134. package/dest/sequencer/checkpoint_builder.d.ts.map +0 -1
  135. package/dest/sequencer/checkpoint_builder.js +0 -131
  136. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  137. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  138. package/dest/tx_validator/nullifier_cache.js +0 -24
  139. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  140. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  141. package/dest/tx_validator/tx_validator_factory.js +0 -53
  142. package/src/sequencer/block_builder.ts +0 -217
  143. package/src/sequencer/checkpoint_builder.ts +0 -217
  144. package/src/tx_validator/nullifier_cache.ts +0 -30
  145. package/src/tx_validator/tx_validator_factory.ts +0 -133
@@ -1,32 +1,404 @@
1
- function _ts_decorate(decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1
+ function applyDecs2203RFactory() {
2
+ function createAddInitializerMethod(initializers, decoratorFinishedRef) {
3
+ return function addInitializer(initializer) {
4
+ assertNotFinished(decoratorFinishedRef, "addInitializer");
5
+ assertCallable(initializer, "An initializer");
6
+ initializers.push(initializer);
7
+ };
8
+ }
9
+ function memberDec(dec, name, desc, initializers, kind, isStatic, isPrivate, metadata, value) {
10
+ var kindStr;
11
+ switch(kind){
12
+ case 1:
13
+ kindStr = "accessor";
14
+ break;
15
+ case 2:
16
+ kindStr = "method";
17
+ break;
18
+ case 3:
19
+ kindStr = "getter";
20
+ break;
21
+ case 4:
22
+ kindStr = "setter";
23
+ break;
24
+ default:
25
+ kindStr = "field";
26
+ }
27
+ var ctx = {
28
+ kind: kindStr,
29
+ name: isPrivate ? "#" + name : name,
30
+ static: isStatic,
31
+ private: isPrivate,
32
+ metadata: metadata
33
+ };
34
+ var decoratorFinishedRef = {
35
+ v: false
36
+ };
37
+ ctx.addInitializer = createAddInitializerMethod(initializers, decoratorFinishedRef);
38
+ var get, set;
39
+ if (kind === 0) {
40
+ if (isPrivate) {
41
+ get = desc.get;
42
+ set = desc.set;
43
+ } else {
44
+ get = function() {
45
+ return this[name];
46
+ };
47
+ set = function(v) {
48
+ this[name] = v;
49
+ };
50
+ }
51
+ } else if (kind === 2) {
52
+ get = function() {
53
+ return desc.value;
54
+ };
55
+ } else {
56
+ if (kind === 1 || kind === 3) {
57
+ get = function() {
58
+ return desc.get.call(this);
59
+ };
60
+ }
61
+ if (kind === 1 || kind === 4) {
62
+ set = function(v) {
63
+ desc.set.call(this, v);
64
+ };
65
+ }
66
+ }
67
+ ctx.access = get && set ? {
68
+ get: get,
69
+ set: set
70
+ } : get ? {
71
+ get: get
72
+ } : {
73
+ set: set
74
+ };
75
+ try {
76
+ return dec(value, ctx);
77
+ } finally{
78
+ decoratorFinishedRef.v = true;
79
+ }
80
+ }
81
+ function assertNotFinished(decoratorFinishedRef, fnName) {
82
+ if (decoratorFinishedRef.v) {
83
+ throw new Error("attempted to call " + fnName + " after decoration was finished");
84
+ }
85
+ }
86
+ function assertCallable(fn, hint) {
87
+ if (typeof fn !== "function") {
88
+ throw new TypeError(hint + " must be a function");
89
+ }
90
+ }
91
+ function assertValidReturnValue(kind, value) {
92
+ var type = typeof value;
93
+ if (kind === 1) {
94
+ if (type !== "object" || value === null) {
95
+ throw new TypeError("accessor decorators must return an object with get, set, or init properties or void 0");
96
+ }
97
+ if (value.get !== undefined) {
98
+ assertCallable(value.get, "accessor.get");
99
+ }
100
+ if (value.set !== undefined) {
101
+ assertCallable(value.set, "accessor.set");
102
+ }
103
+ if (value.init !== undefined) {
104
+ assertCallable(value.init, "accessor.init");
105
+ }
106
+ } else if (type !== "function") {
107
+ var hint;
108
+ if (kind === 0) {
109
+ hint = "field";
110
+ } else if (kind === 10) {
111
+ hint = "class";
112
+ } else {
113
+ hint = "method";
114
+ }
115
+ throw new TypeError(hint + " decorators must return a function or void 0");
116
+ }
117
+ }
118
+ function applyMemberDec(ret, base, decInfo, name, kind, isStatic, isPrivate, initializers, metadata) {
119
+ var decs = decInfo[0];
120
+ var desc, init, value;
121
+ if (isPrivate) {
122
+ if (kind === 0 || kind === 1) {
123
+ desc = {
124
+ get: decInfo[3],
125
+ set: decInfo[4]
126
+ };
127
+ } else if (kind === 3) {
128
+ desc = {
129
+ get: decInfo[3]
130
+ };
131
+ } else if (kind === 4) {
132
+ desc = {
133
+ set: decInfo[3]
134
+ };
135
+ } else {
136
+ desc = {
137
+ value: decInfo[3]
138
+ };
139
+ }
140
+ } else if (kind !== 0) {
141
+ desc = Object.getOwnPropertyDescriptor(base, name);
142
+ }
143
+ if (kind === 1) {
144
+ value = {
145
+ get: desc.get,
146
+ set: desc.set
147
+ };
148
+ } else if (kind === 2) {
149
+ value = desc.value;
150
+ } else if (kind === 3) {
151
+ value = desc.get;
152
+ } else if (kind === 4) {
153
+ value = desc.set;
154
+ }
155
+ var newValue, get, set;
156
+ if (typeof decs === "function") {
157
+ newValue = memberDec(decs, name, desc, initializers, kind, isStatic, isPrivate, metadata, value);
158
+ if (newValue !== void 0) {
159
+ assertValidReturnValue(kind, newValue);
160
+ if (kind === 0) {
161
+ init = newValue;
162
+ } else if (kind === 1) {
163
+ init = newValue.init;
164
+ get = newValue.get || value.get;
165
+ set = newValue.set || value.set;
166
+ value = {
167
+ get: get,
168
+ set: set
169
+ };
170
+ } else {
171
+ value = newValue;
172
+ }
173
+ }
174
+ } else {
175
+ for(var i = decs.length - 1; i >= 0; i--){
176
+ var dec = decs[i];
177
+ newValue = memberDec(dec, name, desc, initializers, kind, isStatic, isPrivate, metadata, value);
178
+ if (newValue !== void 0) {
179
+ assertValidReturnValue(kind, newValue);
180
+ var newInit;
181
+ if (kind === 0) {
182
+ newInit = newValue;
183
+ } else if (kind === 1) {
184
+ newInit = newValue.init;
185
+ get = newValue.get || value.get;
186
+ set = newValue.set || value.set;
187
+ value = {
188
+ get: get,
189
+ set: set
190
+ };
191
+ } else {
192
+ value = newValue;
193
+ }
194
+ if (newInit !== void 0) {
195
+ if (init === void 0) {
196
+ init = newInit;
197
+ } else if (typeof init === "function") {
198
+ init = [
199
+ init,
200
+ newInit
201
+ ];
202
+ } else {
203
+ init.push(newInit);
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ if (kind === 0 || kind === 1) {
210
+ if (init === void 0) {
211
+ init = function(instance, init) {
212
+ return init;
213
+ };
214
+ } else if (typeof init !== "function") {
215
+ var ownInitializers = init;
216
+ init = function(instance, init) {
217
+ var value = init;
218
+ for(var i = 0; i < ownInitializers.length; i++){
219
+ value = ownInitializers[i].call(instance, value);
220
+ }
221
+ return value;
222
+ };
223
+ } else {
224
+ var originalInitializer = init;
225
+ init = function(instance, init) {
226
+ return originalInitializer.call(instance, init);
227
+ };
228
+ }
229
+ ret.push(init);
230
+ }
231
+ if (kind !== 0) {
232
+ if (kind === 1) {
233
+ desc.get = value.get;
234
+ desc.set = value.set;
235
+ } else if (kind === 2) {
236
+ desc.value = value;
237
+ } else if (kind === 3) {
238
+ desc.get = value;
239
+ } else if (kind === 4) {
240
+ desc.set = value;
241
+ }
242
+ if (isPrivate) {
243
+ if (kind === 1) {
244
+ ret.push(function(instance, args) {
245
+ return value.get.call(instance, args);
246
+ });
247
+ ret.push(function(instance, args) {
248
+ return value.set.call(instance, args);
249
+ });
250
+ } else if (kind === 2) {
251
+ ret.push(value);
252
+ } else {
253
+ ret.push(function(instance, args) {
254
+ return value.call(instance, args);
255
+ });
256
+ }
257
+ } else {
258
+ Object.defineProperty(base, name, desc);
259
+ }
260
+ }
261
+ }
262
+ function applyMemberDecs(Class, decInfos, metadata) {
263
+ var ret = [];
264
+ var protoInitializers;
265
+ var staticInitializers;
266
+ var existingProtoNonFields = new Map();
267
+ var existingStaticNonFields = new Map();
268
+ for(var i = 0; i < decInfos.length; i++){
269
+ var decInfo = decInfos[i];
270
+ if (!Array.isArray(decInfo)) continue;
271
+ var kind = decInfo[1];
272
+ var name = decInfo[2];
273
+ var isPrivate = decInfo.length > 3;
274
+ var isStatic = kind >= 5;
275
+ var base;
276
+ var initializers;
277
+ if (isStatic) {
278
+ base = Class;
279
+ kind = kind - 5;
280
+ staticInitializers = staticInitializers || [];
281
+ initializers = staticInitializers;
282
+ } else {
283
+ base = Class.prototype;
284
+ protoInitializers = protoInitializers || [];
285
+ initializers = protoInitializers;
286
+ }
287
+ if (kind !== 0 && !isPrivate) {
288
+ var existingNonFields = isStatic ? existingStaticNonFields : existingProtoNonFields;
289
+ var existingKind = existingNonFields.get(name) || 0;
290
+ if (existingKind === true || existingKind === 3 && kind !== 4 || existingKind === 4 && kind !== 3) {
291
+ throw new Error("Attempted to decorate a public method/accessor that has the same name as a previously decorated public method/accessor. This is not currently supported by the decorators plugin. Property name was: " + name);
292
+ } else if (!existingKind && kind > 2) {
293
+ existingNonFields.set(name, kind);
294
+ } else {
295
+ existingNonFields.set(name, true);
296
+ }
297
+ }
298
+ applyMemberDec(ret, base, decInfo, name, kind, isStatic, isPrivate, initializers, metadata);
299
+ }
300
+ pushInitializers(ret, protoInitializers);
301
+ pushInitializers(ret, staticInitializers);
302
+ return ret;
303
+ }
304
+ function pushInitializers(ret, initializers) {
305
+ if (initializers) {
306
+ ret.push(function(instance) {
307
+ for(var i = 0; i < initializers.length; i++){
308
+ initializers[i].call(instance);
309
+ }
310
+ return instance;
311
+ });
312
+ }
313
+ }
314
+ function applyClassDecs(targetClass, classDecs, metadata) {
315
+ if (classDecs.length > 0) {
316
+ var initializers = [];
317
+ var newClass = targetClass;
318
+ var name = targetClass.name;
319
+ for(var i = classDecs.length - 1; i >= 0; i--){
320
+ var decoratorFinishedRef = {
321
+ v: false
322
+ };
323
+ try {
324
+ var nextNewClass = classDecs[i](newClass, {
325
+ kind: "class",
326
+ name: name,
327
+ addInitializer: createAddInitializerMethod(initializers, decoratorFinishedRef),
328
+ metadata
329
+ });
330
+ } finally{
331
+ decoratorFinishedRef.v = true;
332
+ }
333
+ if (nextNewClass !== undefined) {
334
+ assertValidReturnValue(10, nextNewClass);
335
+ newClass = nextNewClass;
336
+ }
337
+ }
338
+ return [
339
+ defineMetadata(newClass, metadata),
340
+ function() {
341
+ for(var i = 0; i < initializers.length; i++){
342
+ initializers[i].call(newClass);
343
+ }
344
+ }
345
+ ];
346
+ }
347
+ }
348
+ function defineMetadata(Class, metadata) {
349
+ return Object.defineProperty(Class, Symbol.metadata || Symbol.for("Symbol.metadata"), {
350
+ configurable: true,
351
+ enumerable: true,
352
+ value: metadata
353
+ });
354
+ }
355
+ return function applyDecs2203R(targetClass, memberDecs, classDecs, parentClass) {
356
+ if (parentClass !== void 0) {
357
+ var parentMetadata = parentClass[Symbol.metadata || Symbol.for("Symbol.metadata")];
358
+ }
359
+ var metadata = Object.create(parentMetadata === void 0 ? null : parentMetadata);
360
+ var e = applyMemberDecs(targetClass, memberDecs, metadata);
361
+ if (!classDecs.length) defineMetadata(targetClass, metadata);
362
+ return {
363
+ e: e,
364
+ get c () {
365
+ return applyClassDecs(targetClass, classDecs, metadata);
366
+ }
367
+ };
368
+ };
6
369
  }
370
+ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
371
+ return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass);
372
+ }
373
+ var _dec, _dec1, _dec2, _dec3, _initProto;
7
374
  import { getKzg } from '@aztec/blob-lib';
8
- import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
375
+ import { PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
9
376
  import { NoCommitteeError } from '@aztec/ethereum/contracts';
10
- import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
377
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
11
378
  import { merge, omit, pick } from '@aztec/foundation/collection';
12
- import { Fr } from '@aztec/foundation/curves/bn254';
13
379
  import { EthAddress } from '@aztec/foundation/eth-address';
14
380
  import { createLogger } from '@aztec/foundation/log';
15
381
  import { RunningPromise } from '@aztec/foundation/running-promise';
16
- import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
382
+ import { getEpochAtSlot, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
17
383
  import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
18
384
  import { pickFromSchema } from '@aztec/stdlib/schemas';
19
- import { MerkleTreeId } from '@aztec/stdlib/trees';
20
- import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
385
+ import { Attributes, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
21
386
  import EventEmitter from 'node:events';
22
387
  import { DefaultSequencerConfig } from '../config.js';
388
+ import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
23
389
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
390
+ import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
24
391
  import { CheckpointVoter } from './checkpoint_voter.js';
25
392
  import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
26
393
  import { SequencerMetrics } from './metrics.js';
27
394
  import { SequencerTimetable } from './timetable.js';
28
395
  import { SequencerState } from './utils.js';
29
396
  export { SequencerState };
397
+ _dec = trackSpan('Sequencer.work'), _dec1 = trackSpan('Sequencer.prepareCheckpointProposal'), _dec2 = trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot })=>({
398
+ [Attributes.SLOT_NUMBER]: slot
399
+ })), _dec3 = trackSpan('Sequencer.tryVoteWhenEscapeHatchOpen', ({ slot })=>({
400
+ [Attributes.SLOT_NUMBER]: slot
401
+ }));
30
402
  /**
31
403
  * Sequencer client
32
404
  * - Checks whether it is elected as proposer for the next slot
@@ -50,47 +422,78 @@ export { SequencerState };
50
422
  rollupContract;
51
423
  telemetry;
52
424
  log;
425
+ static{
426
+ ({ e: [_initProto] } = _apply_decs_2203_r(this, [
427
+ [
428
+ _dec,
429
+ 2,
430
+ "work"
431
+ ],
432
+ [
433
+ _dec1,
434
+ 2,
435
+ "prepareCheckpointProposal"
436
+ ],
437
+ [
438
+ _dec2,
439
+ 2,
440
+ "tryVoteWhenSyncFails"
441
+ ],
442
+ [
443
+ _dec3,
444
+ 2,
445
+ "tryVoteWhenEscapeHatchOpen"
446
+ ]
447
+ ], []));
448
+ }
53
449
  runningPromise;
54
450
  state;
451
+ stateSlotNumber;
452
+ stateEnteredAtMs;
55
453
  metrics;
56
- /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
454
+ checkpointProposalJobMetrics;
455
+ stateLog;
456
+ /** The last slot for which we attempted to perform our voting duties with degraded block production */ lastSlotForFallbackVote;
457
+ /** The last slot for which we logged "no committee" warning, to avoid spam */ lastSlotForNoCommitteeWarning;
57
458
  /** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */ lastSlotForCheckpointProposalJob;
58
459
  /** Last successful checkpoint proposed */ lastCheckpointProposed;
59
460
  /** The last epoch for which we logged strategy comparison in fisherman mode. */ lastEpochForStrategyComparison;
461
+ /** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */ lastCheckpointProposalJob;
60
462
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
61
- // This shouldn't be here as this gets re-created each time we build/propose a block.
62
- // But we have a number of tests that abuse/rely on this class having a permanent publisher.
63
- // As long as those tests only configure a single publisher they will continue to work.
64
- // This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
65
- // for the block proposer.
66
- // TODO(palla/mbps): Remove this field and fix tests
67
- publisher;
68
463
  /** Config for the sequencer */ config;
464
+ signatureContext;
69
465
  constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, checkpointsBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
70
- super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.checkpointsBuilder = checkpointsBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.telemetry = telemetry, this.log = log, this.state = SequencerState.STOPPED, this.config = DefaultSequencerConfig;
466
+ super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.checkpointsBuilder = checkpointsBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.telemetry = telemetry, this.log = log, this.state = (_initProto(this), SequencerState.STOPPED), this.stateEnteredAtMs = performance.now(), this.config = DefaultSequencerConfig;
467
+ this.stateLog = log.createChild('state');
71
468
  // Add [FISHERMAN] prefix to logger if in fisherman mode
72
469
  if (config.fishermanMode) {
73
470
  this.log = log.createChild('[FISHERMAN]');
74
471
  }
472
+ this.signatureContext = {
473
+ chainId: config.l1ChainId,
474
+ rollupAddress: config.rollupAddress
475
+ };
75
476
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
477
+ this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
76
478
  this.updateConfig(config);
77
479
  }
78
480
  /** Updates sequencer config by the defined values and updates the timetable */ updateConfig(config) {
79
481
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
80
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
482
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
81
483
  this.config = merge(this.config, filteredConfig);
484
+ const p2pPropagationTime = this.config.attestationPropagationTime;
82
485
  this.timetable = new SequencerTimetable({
83
486
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
84
487
  aztecSlotDuration: this.aztecSlotDuration,
85
488
  l1PublishingTime: this.l1PublishingTime,
86
- p2pPropagationTime: this.config.attestationPropagationTime,
489
+ p2pPropagationTime,
87
490
  blockDurationMs: this.config.blockDurationMs,
88
- enforce: this.config.enforceTimeTable
491
+ enforce: this.config.enforceTimeTable,
492
+ pipelining: this.epochCache.isProposerPipeliningEnabled()
89
493
  }, this.metrics, this.log);
90
494
  }
91
- /** Initializes the sequencer (precomputes tables and creates a publisher). Takes about 3s. */ async init() {
495
+ /** Initializes the sequencer (precomputes tables). Takes about 3s. */ init() {
92
496
  getKzg();
93
- this.publisher = (await this.publisherFactory.create(undefined)).publisher;
94
497
  }
95
498
  /** Starts the sequencer and moves to IDLE state. */ start() {
96
499
  this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.config.sequencerPollingIntervalMS);
@@ -100,37 +503,39 @@ export { SequencerState };
100
503
  this.runningPromise.start();
101
504
  this.log.info('Started sequencer');
102
505
  }
506
+ /** Triggers an immediate run of the sequencer, bypassing the polling interval. */ trigger() {
507
+ return this.runningPromise?.trigger();
508
+ }
103
509
  /** Stops the sequencer from building blocks and moves to STOPPED state. */ async stop() {
104
510
  this.log.info(`Stopping sequencer`);
105
511
  this.setState(SequencerState.STOPPING, undefined, {
106
512
  force: true
107
513
  });
108
- this.publisher?.interrupt();
514
+ await this.publisherFactory.stopAll();
109
515
  await this.runningPromise?.stop();
516
+ await this.lastCheckpointProposalJob?.awaitPendingSubmission();
110
517
  this.setState(SequencerState.STOPPED, undefined, {
111
518
  force: true
112
519
  });
113
520
  this.log.info('Stopped sequencer');
114
521
  }
115
- async safeWork() {
522
+ /** Main sequencer loop with a try/catch */ async safeWork() {
116
523
  try {
117
524
  await this.work();
118
525
  } catch (err) {
119
- this.emit('checkpoint-error', {
120
- error: err
121
- });
122
- if (err instanceof SequencerTooSlowError) {
123
- // TODO(palla/mbps): Add missing states
124
- // Log as warn only if we had to abort halfway through the block proposal
125
- const logLvl = [
126
- SequencerState.INITIALIZING_CHECKPOINT,
127
- SequencerState.PROPOSER_CHECK
128
- ].includes(err.proposedState) ? 'debug' : 'warn';
129
- this.log[logLvl](err.message, {
526
+ if (err instanceof SequencerTooSlowError && [
527
+ SequencerState.INITIALIZING_CHECKPOINT,
528
+ SequencerState.PROPOSER_CHECK
529
+ ].includes(err.proposedState)) {
530
+ // No need to alert if we just didn't get to start in time
531
+ this.log.debug(err.message, {
130
532
  now: this.dateProvider.nowInSeconds()
131
533
  });
132
534
  } else {
133
535
  // Re-throw other errors
536
+ this.emit('checkpoint-error', {
537
+ error: err
538
+ });
134
539
  throw err;
135
540
  }
136
541
  } finally{
@@ -151,40 +556,62 @@ export { SequencerState };
151
556
  * - Submit checkpoint
152
557
  */ async work() {
153
558
  this.setState(SequencerState.SYNCHRONIZING, undefined);
154
- const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
559
+ const { slot, targetSlot, epoch, targetEpoch, ts, nowSeconds } = this.getSlotContextInNextL1Slot();
155
560
  // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
156
- const checkpointProposalJob = await this.prepareCheckpointProposal(slot, ts, now);
561
+ const checkpointProposalJob = await this.prepareCheckpointProposal(slot, targetSlot, epoch, targetEpoch, ts, nowSeconds);
157
562
  if (!checkpointProposalJob) {
158
563
  return;
159
564
  }
565
+ // Track the job so we can await its pending L1 submission during shutdown
566
+ this.lastCheckpointProposalJob = checkpointProposalJob;
160
567
  // Execute the checkpoint proposal job
161
568
  const checkpoint = await checkpointProposalJob.execute();
162
569
  // Update last checkpoint proposed (currently unused)
163
570
  if (checkpoint) {
164
571
  this.lastCheckpointProposed = checkpoint;
165
572
  }
166
- // Log fee strategy comparison if on fisherman
167
- if (this.config.fishermanMode && (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)) {
168
- this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
169
- this.lastEpochForStrategyComparison = epoch;
573
+ // Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
574
+ if (this.config.fishermanMode && (this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)) {
575
+ this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
576
+ this.lastEpochForStrategyComparison = targetEpoch;
170
577
  }
171
578
  return checkpoint;
172
579
  }
580
+ /** Returns slot and target slot from a single clock snapshot. */ getSlotContextInNextL1Slot() {
581
+ const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
582
+ const targetSlot = SlotNumber(slot + (this.epochCache.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0));
583
+ return {
584
+ slot,
585
+ targetSlot,
586
+ epoch,
587
+ targetEpoch: getEpochAtSlot(targetSlot, this.l1Constants),
588
+ ts,
589
+ nowSeconds
590
+ };
591
+ }
173
592
  /**
174
593
  * Prepares the checkpoint proposal by performing all necessary checks and setup.
175
594
  * This is the initial step in the main loop.
176
595
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
177
- */ async prepareCheckpointProposal(slot, ts, now) {
178
- // Check we have not already processed this slot (cheapest check)
596
+ */ async prepareCheckpointProposal(slot, targetSlot, epoch, targetEpoch, ts, nowSeconds) {
597
+ // Check we have not already processed this target slot (cheapest check)
179
598
  // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
180
599
  // running against actual time (eg when we use sandbox-style automining)
181
- if (this.lastSlotForCheckpointProposalJob && this.lastSlotForCheckpointProposalJob >= slot && this.config.enforceTimeTable) {
182
- this.log.trace(`Slot ${slot} has already been processed`);
600
+ if (this.lastSlotForCheckpointProposalJob && this.lastSlotForCheckpointProposalJob >= targetSlot && this.config.enforceTimeTable) {
601
+ this.log.trace(`Target slot ${targetSlot} has already been processed`);
183
602
  return undefined;
184
603
  }
185
- // But if we have already proposed for this slot, the we definitely have to skip it, automining or not
186
- if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= slot) {
187
- this.log.trace(`Slot ${slot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`);
604
+ // But if we have already proposed for this slot, then we definitely have to skip it, automining or not
605
+ if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
606
+ this.log.trace(`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`);
607
+ return undefined;
608
+ }
609
+ // Test-only: skip proposing for explicitly paused slots. Attestation paths run in the validator
610
+ // client and are not gated by this hook, so paused proposers still attest to others' proposals.
611
+ if (this.config.pauseProposingForSlots?.some((s)=>s === targetSlot)) {
612
+ this.log.warn(`Skipping proposal for paused slot ${targetSlot} (test-only pauseProposingForSlots hook)`, {
613
+ targetSlot
614
+ });
188
615
  return undefined;
189
616
  }
190
617
  // Check all components are synced to latest as seen by the archiver (queries all subsystems)
@@ -195,36 +622,66 @@ export { SequencerState };
195
622
  if (!syncedTo) {
196
623
  await this.tryVoteWhenSyncFails({
197
624
  slot,
625
+ targetSlot,
198
626
  ts
199
627
  });
200
628
  return undefined;
201
629
  }
202
- // TODO(palla/mbps): Compute proper checkpoint number
203
- const checkpointNumber = CheckpointNumber.fromBlockNumber(BlockNumber(syncedTo.blockNumber + 1));
630
+ // If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
631
+ // Still perform governance/slashing voting (as proposer) once per slot.
632
+ // When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
633
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
634
+ if (isEscapeHatchOpen) {
635
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
636
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
637
+ if (canPropose) {
638
+ await this.tryVoteWhenEscapeHatchOpen({
639
+ slot,
640
+ targetSlot,
641
+ proposer
642
+ });
643
+ } else {
644
+ this.log.trace(`Escape hatch open but we are not proposer, skipping vote-only actions`, {
645
+ slot,
646
+ epoch,
647
+ proposer
648
+ });
649
+ }
650
+ return undefined;
651
+ }
652
+ // Next checkpoint follows from the last synced one
653
+ const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
204
654
  const logCtx = {
205
- now,
206
- syncedToL1Ts: syncedTo.l1Timestamp,
207
- syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
655
+ nowSeconds,
656
+ syncedToL2Slot: syncedTo.syncedL2Slot,
208
657
  slot,
658
+ targetSlot,
209
659
  slotTs: ts,
210
660
  checkpointNumber,
211
661
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
212
662
  };
213
- // Check that we are a proposer for the next slot
663
+ // Check that we are a proposer for the target slot.
214
664
  this.setState(SequencerState.PROPOSER_CHECK, slot);
215
- const [canPropose, proposer] = await this.checkCanPropose(slot);
216
- // If we are not a proposer check if we should invalidate a invalid block, and bail
665
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
666
+ // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
217
667
  if (!canPropose) {
218
- await this.considerInvalidatingBlock(syncedTo, slot);
668
+ await this.considerInvalidatingCheckpoint(syncedTo, slot);
669
+ return undefined;
670
+ }
671
+ // Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
672
+ // confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
673
+ const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
674
+ if (checkpointNumber > confirmedCkpt + 2) {
675
+ this.log.verbose(`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`);
219
676
  return undefined;
220
677
  }
221
- // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
222
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
223
- this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
678
+ // Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
679
+ if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
680
+ this.log.warn(`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`, {
224
681
  ...logCtx,
225
- block: syncedTo.block.header.toInspect()
682
+ block: syncedTo.blockData.header.toInspect()
226
683
  });
227
- this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
684
+ this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
228
685
  return undefined;
229
686
  }
230
687
  // We now need to get ourselves a publisher.
@@ -234,43 +691,110 @@ export { SequencerState };
234
691
  const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
235
692
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
236
693
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
237
- this.publisher = publisher;
238
- // In fisherman mode, set the actual proposer's address for simulations
239
- if (this.config.fishermanMode && proposer) {
240
- publisher.setProposerAddressForSimulation(proposer);
241
- this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
242
- }
243
- // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
244
- // TODO(palla/mbps): We need to invalidate checkpoints, not blocks
245
- const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
246
- // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
247
- // if all the previous checks are good, but we do it just in case.
248
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(syncedTo.archive, proposer ?? EthAddress.ZERO, invalidateBlock);
694
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
695
+ // Prepare invalidation request if the pending chain is invalid (returns undefined if no need).
696
+ // Only simulate invalidation when there's no proposed parent, since we assume the proposed parent
697
+ // will invalidate the currently invalid checkpoint on L1.
698
+ const invalidateCheckpoint = isPipelining && syncedTo.hasProposedCheckpoint || syncedTo.pendingChainValidationStatus.valid ? undefined : await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
699
+ // Determine the correct archive and L1 state overrides for the canProposeAt check.
700
+ // The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
701
+ // When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
702
+ let archiveForCheck = syncedTo.archive;
703
+ if (isPipelining && syncedTo.hasProposedCheckpoint) {
704
+ this.metrics.recordPipelineDepth(syncedTo.checkpointNumber - syncedTo.checkpointedCheckpointNumber);
705
+ this.log.verbose(`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber}) for target slot ${targetSlot}`, {
706
+ targetSlot,
707
+ parentCheckpointNumber: CheckpointNumber(checkpointNumber - 1)
708
+ });
709
+ // Match what L1 will see at archives[pending] once the proposed parent lands: the parent's
710
+ // own archive root from the gossiped proposal. `syncedTo.archive` is the world-state-local
711
+ // view and can transiently diverge from the proposed parent (e.g. before the proposed
712
+ // parent's blocks have been applied locally); diverging here would cause the canProposeAt
713
+ // override to set archives[pending] to one value while we present another for comparison.
714
+ archiveForCheck = syncedTo.proposedCheckpointData.archive.root;
715
+ } else if (invalidateCheckpoint) {
716
+ // After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
717
+ // exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
718
+ archiveForCheck = invalidateCheckpoint.lastArchive;
719
+ this.metrics.recordPipelineDepth(0);
720
+ } else {
721
+ this.metrics.recordPipelineDepth(0);
722
+ }
723
+ // Build the simulation plan: pending/proven override from pipelining or invalidation (or the
724
+ // current snapshot when neither applies, to short-circuit any pending prune in simulation),
725
+ // plus the parent checkpoint cell and fee header when pipelining.
726
+ const simulationOverridesPlan = await buildCheckpointSimulationOverridesPlan({
727
+ checkpointNumber,
728
+ proposedCheckpointData: isPipelining && syncedTo.hasProposedCheckpoint ? syncedTo.proposedCheckpointData : undefined,
729
+ invalidateToPendingCheckpointNumber: invalidateCheckpoint?.forcePendingCheckpointNumber,
730
+ checkpointedCheckpointNumber: syncedTo.checkpointedCheckpointNumber,
731
+ rollup: this.rollupContract,
732
+ signatureContext: this.signatureContext,
733
+ log: this.log
734
+ });
735
+ // The plan always pins both pending/proven (to short-circuit `canPruneAtTime` in simulation),
736
+ // so `provenOverride` always reflects the assumed proven checkpoint we are pinning the
737
+ // simulation to. We additionally warn when the pin is load-bearing — i.e. when a prune would
738
+ // actually fire at the target slot without it — so observers can spot "we are building
739
+ // optimistically across a pruning boundary" in the logs.
740
+ const provenOverride = simulationOverridesPlan?.chainTipsOverride?.proven;
741
+ if (provenOverride !== undefined && await this.l2BlockSource.isPruneDueAtSlot(targetSlot)) {
742
+ this.log.warn(`Assuming proof for epoch ending at checkpoint ${provenOverride} lands by target slot ${targetSlot}`, {
743
+ checkpointNumber,
744
+ slot,
745
+ targetSlot,
746
+ provenOverride
747
+ });
748
+ }
749
+ this.emit('preparing-checkpoint', {
750
+ targetSlot,
751
+ checkpointNumber,
752
+ hadProposedParent: this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint,
753
+ provenOverride,
754
+ simulatedPending: simulationOverridesPlan?.chainTipsOverride?.pending
755
+ });
756
+ const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, simulationOverridesPlan);
757
+ const proposeContext = {
758
+ hasProposedCheckpoint: syncedTo.hasProposedCheckpoint,
759
+ proposedCheckpointNumber: syncedTo.proposedCheckpointData?.checkpointNumber,
760
+ checkpointedCheckpointNumber: syncedTo.checkpointedCheckpointNumber,
761
+ isInvalidating: !!invalidateCheckpoint,
762
+ invalidatingCheckpointNumber: invalidateCheckpoint?.checkpointNumber,
763
+ archiveForCheck: archiveForCheck.toString(),
764
+ overridePendingCheckpointNumber: simulationOverridesPlan?.chainTipsOverride?.pending,
765
+ overrideArchive: simulationOverridesPlan?.pendingCheckpointState?.archive,
766
+ overrideFeeHeader: simulationOverridesPlan?.pendingCheckpointState?.feeHeader
767
+ };
249
768
  if (canProposeCheck === undefined) {
250
- this.log.warn(`Cannot propose checkpoint ${checkpointNumber} at slot ${slot} due to failed rollup contract check`, logCtx);
769
+ this.log.warn(`Cannot propose checkpoint ${checkpointNumber} at slot ${slot} due to failed rollup contract check`, {
770
+ ...logCtx,
771
+ ...proposeContext
772
+ });
251
773
  this.emit('proposer-rollup-check-failed', {
252
774
  reason: 'Rollup contract check failed',
253
775
  slot
254
776
  });
255
- this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
777
+ this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
256
778
  return undefined;
257
779
  }
258
- if (canProposeCheck.slot !== slot) {
259
- this.log.warn(`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${slot} but got ${canProposeCheck.slot}.`, {
780
+ if (canProposeCheck.slot !== targetSlot) {
781
+ this.log.warn(`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${targetSlot} but got ${canProposeCheck.slot}.`, {
260
782
  ...logCtx,
783
+ ...proposeContext,
261
784
  rollup: canProposeCheck,
262
- expectedSlot: slot
785
+ expectedSlot: targetSlot
263
786
  });
264
787
  this.emit('proposer-rollup-check-failed', {
265
788
  reason: 'Slot mismatch',
266
789
  slot
267
790
  });
268
- this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
791
+ this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
269
792
  return undefined;
270
793
  }
271
794
  if (canProposeCheck.checkpointNumber !== checkpointNumber) {
272
795
  this.log.warn(`Cannot propose due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected checkpoint ${checkpointNumber} but got ${canProposeCheck.checkpointNumber}.`, {
273
796
  ...logCtx,
797
+ ...proposeContext,
274
798
  rollup: canProposeCheck,
275
799
  expectedSlot: slot
276
800
  });
@@ -278,19 +802,27 @@ export { SequencerState };
278
802
  reason: 'Block mismatch',
279
803
  slot
280
804
  });
281
- this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
805
+ this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
282
806
  return undefined;
283
807
  }
284
- this.lastSlotForCheckpointProposalJob = slot;
285
- this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, {
808
+ this.lastSlotForCheckpointProposalJob = targetSlot;
809
+ await this.p2pClient.prepareForSlot(targetSlot);
810
+ this.log.info(`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`, {
286
811
  ...logCtx,
287
- proposer
812
+ ...proposeContext,
813
+ proposer,
814
+ pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled()
288
815
  });
289
816
  // Create and return the checkpoint proposal job
290
- return this.createCheckpointProposalJob(slot, checkpointNumber, syncedTo.blockNumber, proposer, publisher, attestorAddress, invalidateBlock);
817
+ return this.createCheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedTo.blockNumber, syncedTo.checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, syncedTo.proposedCheckpointData);
291
818
  }
292
- createCheckpointProposalJob(slot, checkpointNumber, syncedToBlockNumber, proposer, publisher, attestorAddress, invalidateBlock) {
293
- return new CheckpointProposalJob(slot, checkpointNumber, syncedToBlockNumber, proposer, publisher, attestorAddress, invalidateBlock, this.validatorClient, this.globalsBuilder, this.p2pClient, this.worldState, this.l1ToL2MessageSource, this.checkpointsBuilder, this.l1Constants, this.config, this.timetable, this.slasherClient, this.epochCache, this.dateProvider, this.metrics, this, this.setState.bind(this), this.log);
819
+ createCheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, proposedCheckpointData) {
820
+ return new CheckpointProposalJob(slot, targetSlot, targetEpoch, checkpointNumber, syncedToBlockNumber, checkpointedCheckpointNumber, proposer, publisher, attestorAddress, invalidateCheckpoint, this.validatorClient, this.globalsBuilder, this.p2pClient, this.worldState, this.l1ToL2MessageSource, this.l2BlockSource, this.checkpointsBuilder, this.l2BlockSource, this.l1Constants, this.signatureContext, this.config, this.timetable, this.slasherClient, this.epochCache, this.dateProvider, this.metrics, this.checkpointProposalJobMetrics.createRecorder(), this, this.setState.bind(this), this.tracer, this.log.getBindings(), proposedCheckpointData);
821
+ }
822
+ /**
823
+ * Returns the current sequencer state.
824
+ */ getState() {
825
+ return this.state;
294
826
  }
295
827
  /**
296
828
  * Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
@@ -311,37 +843,52 @@ export { SequencerState };
311
843
  secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
312
844
  this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
313
845
  }
846
+ const oldState = this.state;
847
+ const oldStateSlotNumber = this.stateSlotNumber;
848
+ const stateChanged = proposedState !== oldState;
849
+ const transitionAtMs = performance.now();
850
+ const stateDurationMs = transitionAtMs - this.stateEnteredAtMs;
314
851
  const boringStates = [
315
852
  SequencerState.IDLE,
316
853
  SequencerState.SYNCHRONIZING
317
854
  ];
318
- const logLevel = boringStates.includes(proposedState) && boringStates.includes(this.state) ? 'trace' : 'debug';
319
- this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, {
855
+ const logLevel = boringStates.includes(proposedState) && boringStates.includes(oldState) ? 'trace' : 'debug';
856
+ this.stateLog[logLevel](`Transitioning from ${oldState} to ${proposedState}`, {
857
+ oldState,
858
+ newState: proposedState,
320
859
  slotNumber,
321
- secondsIntoSlot
860
+ stateSlotNumber: oldStateSlotNumber,
861
+ secondsIntoSlot,
862
+ ...stateChanged && {
863
+ stateDurationMs: Math.ceil(stateDurationMs)
864
+ }
322
865
  });
323
866
  this.emit('state-changed', {
324
- oldState: this.state,
867
+ oldState,
325
868
  newState: proposedState,
326
869
  secondsIntoSlot,
327
870
  slot: slotNumber
328
871
  });
872
+ if (stateChanged) {
873
+ this.metrics.recordStateDuration(stateDurationMs, oldState);
874
+ this.stateEnteredAtMs = transitionAtMs;
875
+ this.stateSlotNumber = slotNumber;
876
+ }
329
877
  this.state = proposedState;
330
878
  }
331
879
  /**
332
880
  * Returns whether all dependencies have caught up.
333
881
  * We don't check against the previous block submitted since it may have been reorg'd out.
334
882
  */ async checkSync(args) {
335
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
336
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
337
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
338
- const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
339
- const { slot, ts } = args;
340
- if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
883
+ // Check that the archiver has fully synced the L2 slot before the one we want to propose in.
884
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
885
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
886
+ const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
887
+ const { slot } = args;
888
+ if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
341
889
  this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
342
890
  slot,
343
- ts,
344
- l1Timestamp
891
+ syncedL2Slot
345
892
  });
346
893
  return undefined;
347
894
  }
@@ -350,65 +897,120 @@ export { SequencerState };
350
897
  number: syncSummary.latestBlockNumber,
351
898
  hash: syncSummary.latestBlockHash
352
899
  })),
353
- this.l2BlockSource.getL2Tips().then((t)=>t.latest),
900
+ this.l2BlockSource.getL2Tips().then((t)=>({
901
+ proposed: t.proposed,
902
+ checkpointed: t.checkpointed,
903
+ proposedCheckpoint: t.proposedCheckpoint
904
+ })),
354
905
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
355
- this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
356
- this.l2BlockSource.getPendingChainValidationStatus()
906
+ this.l1ToL2MessageSource.getL2Tips().then((t)=>({
907
+ proposed: t.proposed,
908
+ checkpointed: t.checkpointed
909
+ })),
910
+ this.l2BlockSource.getPendingChainValidationStatus(),
911
+ this.l2BlockSource.getProposedCheckpointData()
357
912
  ]);
358
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
359
- // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
360
- // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
361
- const result = l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 || worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
913
+ const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] = syncedBlocks;
914
+ const result = worldState.hash === l2Tips.proposed.hash && p2p.hash === l2Tips.proposed.hash && l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash && l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash && l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash;
362
915
  if (!result) {
363
916
  this.log.debug(`Sequencer sync check failed`, {
364
917
  worldState,
365
- l2BlockSource,
918
+ l2BlockSource: l2Tips.proposed,
366
919
  p2p,
367
- l1ToL2MessageSource
920
+ l1ToL2MessageSourceTips
368
921
  });
369
922
  return undefined;
370
923
  }
371
- // Special case for genesis state
372
924
  const blockNumber = worldState.number;
373
- if (blockNumber < INITIAL_L2_BLOCK_NUM) {
374
- const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
375
- return {
376
- blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1),
377
- archive,
378
- l1Timestamp,
379
- pendingChainValidationStatus
380
- };
925
+ const blockData = await this.l2BlockSource.getBlockData({
926
+ number: blockNumber
927
+ });
928
+ if (!blockData) {
929
+ this.log.warn(`Sequencer sync check failed: failed to get L2 block data ${blockNumber} from the archiver`, {
930
+ blockNumber,
931
+ l2Tips,
932
+ syncedL2Slot,
933
+ ...args
934
+ });
935
+ return undefined;
936
+ }
937
+ // Refuse to build a checkpoint on top of a proposed block whose enclosing checkpoint was never
938
+ // proposed. Under pipelining we may have received and reexecuted such a block locally — advancing
939
+ // our world-state tip past the checkpointed tip — while the proposing node never published the
940
+ // matching proposed checkpoint (e.g. it crashed before assembling it). Building on this orphan block
941
+ // would fork the chain off a tip no other node can follow. The archiver prunes these orphan blocks
942
+ // once their build slot ends; this guard is the correctness barrier during the grace window before.
943
+ if (blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number && (l2Tips.proposedCheckpoint.checkpoint.number !== blockData.checkpointNumber || proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber)) {
944
+ this.log.warn(`Sequencer sync check failed: proposed block has no matching proposed checkpoint`, {
945
+ blockCheckpointNumber: blockData.checkpointNumber,
946
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
947
+ proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
948
+ proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
949
+ blockNumber: blockData.header.getBlockNumber(),
950
+ blockSlot: blockData.header.getSlot(),
951
+ syncedL2Slot,
952
+ ...args
953
+ });
954
+ return undefined;
381
955
  }
382
- const block = await this.l2BlockSource.getL2BlockNew(blockNumber);
383
- if (!block) {
384
- // this shouldn't really happen because a moment ago we checked that all components were in sync
385
- this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
956
+ const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
957
+ // The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots
958
+ // (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver
959
+ // write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but
960
+ // no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that
961
+ // window — the next checkSync tick will see a coherent snapshot.
962
+ if (hasProposedCheckpoint && (!proposedCheckpointData || proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number)) {
963
+ this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, {
964
+ proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
965
+ checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number,
966
+ proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
967
+ syncedL2Slot,
968
+ ...args
969
+ });
970
+ return undefined;
971
+ }
972
+ // Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building
973
+ // The checkpoint number to build is derived as blockData.checkpointNumber + 1
974
+ if (proposedCheckpointData && proposedCheckpointData.checkpointNumber !== blockData.checkpointNumber) {
975
+ this.log.warn(`Sequencer sync check failed: proposed checkpoint number mismatch`, {
976
+ proposedCheckpointNumber: proposedCheckpointData.checkpointNumber,
977
+ blockCheckpointNumber: blockData.checkpointNumber,
978
+ syncedL2Slot,
979
+ ...args
980
+ });
386
981
  return undefined;
387
982
  }
388
983
  return {
389
- block,
390
- blockNumber: block.number,
391
- archive: block.archive.root,
392
- l1Timestamp,
984
+ blockData,
985
+ blockNumber: blockData.header.getBlockNumber(),
986
+ checkpointNumber: blockData.checkpointNumber,
987
+ checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
988
+ archive: blockData.archive.root,
989
+ hasProposedCheckpoint,
990
+ proposedCheckpointData,
991
+ syncedL2Slot,
393
992
  pendingChainValidationStatus
394
993
  };
395
994
  }
396
995
  /**
397
996
  * Checks if we are the proposer for the next slot.
398
997
  * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
399
- */ async checkCanPropose(slot) {
998
+ */ async checkCanPropose(targetSlot) {
400
999
  let proposer;
401
1000
  try {
402
- proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
1001
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
403
1002
  } catch (e) {
404
1003
  if (e instanceof NoCommitteeError) {
405
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
1004
+ if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
1005
+ this.lastSlotForNoCommitteeWarning = targetSlot;
1006
+ this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
1007
+ }
406
1008
  return [
407
1009
  false,
408
1010
  undefined
409
1011
  ];
410
1012
  }
411
- this.log.error(`Error getting proposer for slot ${slot}`, e);
1013
+ this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
412
1014
  return [
413
1015
  false,
414
1016
  undefined
@@ -431,7 +1033,8 @@ export { SequencerState };
431
1033
  const validatorAddresses = this.validatorClient.getValidatorAddresses();
432
1034
  const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
433
1035
  if (!weAreProposer) {
434
- this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, {
1036
+ this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
1037
+ targetSlot,
435
1038
  validatorAddresses,
436
1039
  proposer
437
1040
  });
@@ -440,6 +1043,10 @@ export { SequencerState };
440
1043
  proposer
441
1044
  ];
442
1045
  }
1046
+ this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
1047
+ targetSlot,
1048
+ proposer
1049
+ });
443
1050
  return [
444
1051
  true,
445
1052
  proposer
@@ -449,9 +1056,9 @@ export { SequencerState };
449
1056
  * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
450
1057
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
451
1058
  */ async tryVoteWhenSyncFails(args) {
452
- const { slot } = args;
1059
+ const { slot, targetSlot } = args;
453
1060
  // Prevent duplicate attempts in the same slot
454
- if (this.lastSlotForVoteWhenSyncFailed === slot) {
1061
+ if (this.lastSlotForFallbackVote === slot) {
455
1062
  this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
456
1063
  return;
457
1064
  }
@@ -472,7 +1079,7 @@ export { SequencerState };
472
1079
  maxAllowedTime
473
1080
  });
474
1081
  // Check if we're a proposer or proposal is open
475
- const [canPropose, proposer] = await this.checkCanPropose(slot);
1082
+ const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
476
1083
  if (!canPropose) {
477
1084
  this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, {
478
1085
  slot,
@@ -481,15 +1088,15 @@ export { SequencerState };
481
1088
  return;
482
1089
  }
483
1090
  // Mark this slot as attempted
484
- this.lastSlotForVoteWhenSyncFailed = slot;
1091
+ this.lastSlotForFallbackVote = slot;
485
1092
  // Get a publisher for voting
486
1093
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
487
1094
  this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
488
1095
  attestorAddress,
489
1096
  slot
490
1097
  });
491
- // Enqueue governance and slashing votes
492
- const voter = new CheckpointVoter(slot, publisher, attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log);
1098
+ // Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
1099
+ const voter = new CheckpointVoter(targetSlot, publisher, attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log);
493
1100
  const votesPromises = voter.enqueueVotes();
494
1101
  const votes = await Promise.all(votesPromises);
495
1102
  if (votes.every((p)=>!p)) {
@@ -499,27 +1106,81 @@ export { SequencerState };
499
1106
  this.log.info(`Voting in slot ${slot} despite sync failure`, {
500
1107
  slot
501
1108
  });
502
- await publisher.sendRequests();
1109
+ // Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
1110
+ // expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
1111
+ // slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
1112
+ // work loop while waiting for the target slot to start.
1113
+ void publisher.sendRequestsAt(targetSlot).catch((err)=>{
1114
+ this.log.error(`Failed to publish votes despite sync failure for slot ${slot}`, err, {
1115
+ slot
1116
+ });
1117
+ });
1118
+ }
1119
+ /**
1120
+ * Tries to vote on slashing actions and governance proposals when escape hatch is open.
1121
+ * This allows the sequencer to participate in voting without performing checkpoint proposal work.
1122
+ */ async tryVoteWhenEscapeHatchOpen(args) {
1123
+ const { slot, targetSlot, proposer } = args;
1124
+ // Prevent duplicate attempts in the same slot
1125
+ if (this.lastSlotForFallbackVote === slot) {
1126
+ this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
1127
+ return;
1128
+ }
1129
+ // Mark this slot as attempted
1130
+ this.lastSlotForFallbackVote = slot;
1131
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
1132
+ this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, {
1133
+ slot,
1134
+ targetSlot,
1135
+ attestorAddress
1136
+ });
1137
+ // Under proposer pipelining, the multicall is expected to mine in `targetSlot` (slot + 1).
1138
+ // Governance and slashing votes are EIP-712-signed against the slot they will mine in, and the
1139
+ // L1 contract checks `msg.sender == getCurrentProposer()` using the mining slot. So we must
1140
+ // sign for `targetSlot` and delay submission to the start of `targetSlot`. When pipelining is
1141
+ // disabled `targetSlot == slot` and `sendRequestsAt` resolves with no extra sleep.
1142
+ const voter = new CheckpointVoter(targetSlot, publisher, attestorAddress, this.validatorClient, this.slasherClient, this.l1Constants, this.config, this.metrics, this.log);
1143
+ const votesPromises = voter.enqueueVotes();
1144
+ const votes = await Promise.all(votesPromises);
1145
+ if (votes.every((p)=>!p)) {
1146
+ this.log.debug(`No votes to enqueue for slot ${slot} (escape hatch open)`);
1147
+ return;
1148
+ }
1149
+ this.log.info(`Voting in slot ${slot} (escape hatch open)`, {
1150
+ slot,
1151
+ targetSlot
1152
+ });
1153
+ // Votes are EIP-712-signed for `targetSlot`. Delay submission to the start of `targetSlot` so
1154
+ // the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
1155
+ // `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
1156
+ // silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
1157
+ // waiting for the target slot to start, mirroring tryVoteWhenSyncFails.
1158
+ void publisher.sendRequestsAt(targetSlot).catch((err)=>{
1159
+ this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, {
1160
+ slot,
1161
+ targetSlot
1162
+ });
1163
+ });
503
1164
  }
504
1165
  /**
505
1166
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
506
1167
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
507
1168
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
508
1169
  * and if they fail, any sequencer will try as well.
509
- */ async considerInvalidatingBlock(syncedTo, currentSlot) {
510
- const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
1170
+ */ async considerInvalidatingCheckpoint(syncedTo, currentSlot) {
1171
+ const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
511
1172
  if (pendingChainValidationStatus.valid) {
512
1173
  return;
513
1174
  }
514
- const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
515
- const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
516
- const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
1175
+ const invalidCheckpointNumber = pendingChainValidationStatus.checkpoint.checkpointNumber;
1176
+ const invalidCheckpointTimestamp = pendingChainValidationStatus.checkpoint.timestamp;
1177
+ const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidCheckpointTimestamp);
517
1178
  const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
518
1179
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
519
1180
  const logData = {
520
- invalidL1Timestamp: invalidBlockTimestamp,
521
- l1Timestamp,
522
- invalidBlock: pendingChainValidationStatus.block,
1181
+ invalidL1Timestamp: invalidCheckpointTimestamp,
1182
+ syncedL2Slot,
1183
+ invalidCheckpoint: pendingChainValidationStatus.checkpoint,
523
1184
  secondsBeforeInvalidatingBlockAsCommitteeMember,
524
1185
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
525
1186
  ourValidatorAddresses,
@@ -547,17 +1208,17 @@ export { SequencerState };
547
1208
  validatorToUse = ourValidatorAddresses[0];
548
1209
  }
549
1210
  const { publisher } = await this.publisherFactory.create(validatorToUse);
550
- const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
551
- if (!invalidateBlock) {
552
- this.log.warn(`Failed to simulate invalidate block`, logData);
1211
+ const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(pendingChainValidationStatus);
1212
+ if (!invalidateCheckpoint) {
1213
+ this.log.warn(`Failed to simulate invalidate checkpoint`, logData);
553
1214
  return;
554
1215
  }
555
- this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
556
- publisher.enqueueInvalidateBlock(invalidateBlock);
1216
+ this.log.info(invalidateAsCommitteeMember ? `Invalidating checkpoint ${invalidCheckpointNumber} as committee member` : `Invalidating checkpoint ${invalidCheckpointNumber} as non-committee member`, logData);
1217
+ publisher.enqueueInvalidateCheckpoint(invalidateCheckpoint);
557
1218
  if (!this.config.fishermanMode) {
558
1219
  await publisher.sendRequests();
559
1220
  } else {
560
- this.log.info('Invalidating block in fisherman mode, clearing pending requests');
1221
+ this.log.info('Invalidating checkpoint in fisherman mode, clearing pending requests');
561
1222
  publisher.clearPendingRequests();
562
1223
  }
563
1224
  }
@@ -609,6 +1270,9 @@ export { SequencerState };
609
1270
  getValidatorAddresses() {
610
1271
  return this.validatorClient?.getValidatorAddresses();
611
1272
  }
1273
+ /** Updates the publisher factory's node keystore adapter after a keystore reload. */ updatePublisherNodeKeyStore(adapter) {
1274
+ this.publisherFactory.updateNodeKeyStore(adapter);
1275
+ }
612
1276
  getConfig() {
613
1277
  return this.config;
614
1278
  }
@@ -616,6 +1280,3 @@ export { SequencerState };
616
1280
  return this.config.l1PublishingTime ?? this.l1Constants.ethereumSlotDuration;
617
1281
  }
618
1282
  }
619
- _ts_decorate([
620
- trackSpan('Sequencer.work')
621
- ], Sequencer.prototype, "safeWork", null);