@aztec/sequencer-client 0.0.1-commit.9b94fc1 → 0.0.1-commit.9ee6fcc6

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 (118) hide show
  1. package/dest/client/sequencer-client.d.ts +24 -16
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +73 -32
  4. package/dest/config.d.ts +35 -9
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +106 -44
  7. package/dest/global_variable_builder/global_builder.d.ts +27 -14
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +65 -54
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/index.d.ts +2 -3
  13. package/dest/index.d.ts.map +1 -1
  14. package/dest/index.js +1 -2
  15. package/dest/publisher/config.d.ts +53 -20
  16. package/dest/publisher/config.d.ts.map +1 -1
  17. package/dest/publisher/config.js +124 -39
  18. package/dest/publisher/index.d.ts +2 -1
  19. package/dest/publisher/index.d.ts.map +1 -1
  20. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  21. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  24. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  26. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  27. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  29. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  30. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  31. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  32. package/dest/publisher/sequencer-publisher-factory.d.ts +15 -6
  33. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher-factory.js +28 -3
  35. package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
  36. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  38. package/dest/publisher/sequencer-publisher.d.ts +79 -49
  39. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  40. package/dest/publisher/sequencer-publisher.js +932 -155
  41. package/dest/sequencer/checkpoint_proposal_job.d.ts +108 -0
  42. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  43. package/dest/sequencer/checkpoint_proposal_job.js +1289 -0
  44. package/dest/sequencer/checkpoint_voter.d.ts +35 -0
  45. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
  46. package/dest/sequencer/checkpoint_voter.js +109 -0
  47. package/dest/sequencer/config.d.ts +3 -2
  48. package/dest/sequencer/config.d.ts.map +1 -1
  49. package/dest/sequencer/events.d.ts +47 -0
  50. package/dest/sequencer/events.d.ts.map +1 -0
  51. package/dest/sequencer/events.js +1 -0
  52. package/dest/sequencer/index.d.ts +4 -2
  53. package/dest/sequencer/index.d.ts.map +1 -1
  54. package/dest/sequencer/index.js +3 -1
  55. package/dest/sequencer/metrics.d.ts +42 -6
  56. package/dest/sequencer/metrics.d.ts.map +1 -1
  57. package/dest/sequencer/metrics.js +227 -72
  58. package/dest/sequencer/sequencer.d.ts +125 -134
  59. package/dest/sequencer/sequencer.d.ts.map +1 -1
  60. package/dest/sequencer/sequencer.js +754 -652
  61. package/dest/sequencer/timetable.d.ts +54 -16
  62. package/dest/sequencer/timetable.d.ts.map +1 -1
  63. package/dest/sequencer/timetable.js +147 -62
  64. package/dest/sequencer/types.d.ts +3 -0
  65. package/dest/sequencer/types.d.ts.map +1 -0
  66. package/dest/sequencer/types.js +1 -0
  67. package/dest/sequencer/utils.d.ts +14 -8
  68. package/dest/sequencer/utils.d.ts.map +1 -1
  69. package/dest/sequencer/utils.js +7 -4
  70. package/dest/test/index.d.ts +6 -7
  71. package/dest/test/index.d.ts.map +1 -1
  72. package/dest/test/mock_checkpoint_builder.d.ts +95 -0
  73. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  74. package/dest/test/mock_checkpoint_builder.js +231 -0
  75. package/dest/test/utils.d.ts +53 -0
  76. package/dest/test/utils.d.ts.map +1 -0
  77. package/dest/test/utils.js +104 -0
  78. package/package.json +31 -30
  79. package/src/client/sequencer-client.ts +97 -55
  80. package/src/config.ts +124 -53
  81. package/src/global_variable_builder/global_builder.ts +76 -73
  82. package/src/global_variable_builder/index.ts +1 -1
  83. package/src/index.ts +1 -7
  84. package/src/publisher/config.ts +163 -50
  85. package/src/publisher/index.ts +3 -0
  86. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  87. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  88. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  89. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  90. package/src/publisher/sequencer-publisher-factory.ts +43 -10
  91. package/src/publisher/sequencer-publisher-metrics.ts +19 -71
  92. package/src/publisher/sequencer-publisher.ts +635 -200
  93. package/src/sequencer/README.md +531 -0
  94. package/src/sequencer/checkpoint_proposal_job.ts +1049 -0
  95. package/src/sequencer/checkpoint_voter.ts +130 -0
  96. package/src/sequencer/config.ts +2 -1
  97. package/src/sequencer/events.ts +27 -0
  98. package/src/sequencer/index.ts +3 -1
  99. package/src/sequencer/metrics.ts +282 -82
  100. package/src/sequencer/sequencer.ts +521 -859
  101. package/src/sequencer/timetable.ts +178 -83
  102. package/src/sequencer/types.ts +6 -0
  103. package/src/sequencer/utils.ts +18 -9
  104. package/src/test/index.ts +5 -6
  105. package/src/test/mock_checkpoint_builder.ts +323 -0
  106. package/src/test/utils.ts +167 -0
  107. package/dest/sequencer/block_builder.d.ts +0 -27
  108. package/dest/sequencer/block_builder.d.ts.map +0 -1
  109. package/dest/sequencer/block_builder.js +0 -134
  110. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  111. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  112. package/dest/tx_validator/nullifier_cache.js +0 -24
  113. package/dest/tx_validator/tx_validator_factory.d.ts +0 -17
  114. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  115. package/dest/tx_validator/tx_validator_factory.js +0 -53
  116. package/src/sequencer/block_builder.ts +0 -222
  117. package/src/tx_validator/nullifier_cache.ts +0 -30
  118. package/src/tx_validator/tx_validator_factory.ts +0 -132
@@ -1,19 +1,400 @@
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
+ };
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, _initProto;
1
374
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
2
- import { createBlobSinkClient } from '@aztec/blob-sink/client';
3
- import { FormattedViemError, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract, WEI_CONST, formatViemError, tryExtractEvent } from '@aztec/ethereum';
375
+ import { FeeAssetPriceOracle, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
376
+ import { L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
377
+ import { MAX_L1_TX_LIMIT, WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
378
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
4
379
  import { sumBigint } from '@aztec/foundation/bigint';
5
380
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
6
- import { SlotNumber } from '@aztec/foundation/branded-types';
381
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
382
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
383
+ import { pick } from '@aztec/foundation/collection';
384
+ import { TimeoutError } from '@aztec/foundation/error';
7
385
  import { EthAddress } from '@aztec/foundation/eth-address';
8
386
  import { Signature } from '@aztec/foundation/eth-signature';
9
387
  import { createLogger } from '@aztec/foundation/log';
388
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
10
389
  import { bufferToHex } from '@aztec/foundation/string';
11
390
  import { Timer } from '@aztec/foundation/timer';
12
391
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
13
392
  import { encodeSlashConsensusVotes } from '@aztec/slasher';
14
- import { CommitteeAttestation, CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
15
- import { getTelemetryClient } from '@aztec/telemetry-client';
16
- import { encodeFunctionData, toHex } from 'viem';
393
+ import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
394
+ import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
395
+ import { getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
396
+ import { encodeFunctionData, keccak256, multicall3Abi, toHex } from 'viem';
397
+ import { createL1TxFailedStore } from './l1_tx_failed_store/index.js';
17
398
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
18
399
  export const Actions = [
19
400
  'invalidate-by-invalid-attestation',
@@ -28,22 +409,46 @@ export const Actions = [
28
409
  ];
29
410
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
30
411
  export const compareActions = (a, b)=>Actions.indexOf(a) - Actions.indexOf(b);
412
+ _dec = trackSpan('SequencerPublisher.sendRequests'), _dec1 = trackSpan('SequencerPublisher.validateBlockHeader'), _dec2 = trackSpan('SequencerPublisher.validateCheckpointForSubmission');
31
413
  export class SequencerPublisher {
32
414
  config;
415
+ static{
416
+ ({ e: [_initProto] } = _apply_decs_2203_r(this, [
417
+ [
418
+ _dec,
419
+ 2,
420
+ "sendRequests"
421
+ ],
422
+ [
423
+ _dec1,
424
+ 2,
425
+ "validateBlockHeader"
426
+ ],
427
+ [
428
+ _dec2,
429
+ 2,
430
+ "validateCheckpointForSubmission"
431
+ ]
432
+ ], []));
433
+ }
33
434
  interrupted;
34
435
  metrics;
35
436
  epochCache;
437
+ failedTxStore;
36
438
  governanceLog;
37
439
  slashingLog;
38
440
  lastActions;
441
+ isPayloadEmptyCache;
442
+ payloadProposedCache;
39
443
  log;
40
444
  ethereumSlotDuration;
41
- blobSinkClient;
445
+ aztecSlotDuration;
446
+ dateProvider;
447
+ blobClient;
42
448
  /** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
43
- // @note - with blobs, the below estimate seems too large.
44
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
45
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
46
- static PROPOSE_GAS_GUESS = 12_000_000n;
449
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */ getNextPublisher;
450
+ /** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
451
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */ feeAssetPriceOracle;
47
452
  // A CALL to a cold address is 2700 gas
48
453
  static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
49
454
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
@@ -53,24 +458,29 @@ export class SequencerPublisher {
53
458
  govProposerContract;
54
459
  slashingProposerContract;
55
460
  slashFactoryContract;
461
+ tracer;
56
462
  requests;
57
463
  constructor(config, deps){
58
464
  this.config = config;
59
- this.interrupted = false;
465
+ this.interrupted = (_initProto(this), false);
60
466
  this.governanceLog = createLogger('sequencer:publisher:governance');
61
467
  this.slashingLog = createLogger('sequencer:publisher:slashing');
62
468
  this.lastActions = {};
469
+ this.isPayloadEmptyCache = new Map();
470
+ this.payloadProposedCache = new Set();
63
471
  this.requests = [];
64
472
  this.log = deps.log ?? createLogger('sequencer:publisher');
65
473
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
474
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
475
+ this.dateProvider = deps.dateProvider;
66
476
  this.epochCache = deps.epochCache;
67
477
  this.lastActions = deps.lastActions;
68
- this.blobSinkClient = deps.blobSinkClient ?? createBlobSinkClient(config, {
69
- logger: createLogger('sequencer:blob-sink:client')
70
- });
478
+ this.blobClient = deps.blobClient;
71
479
  const telemetry = deps.telemetry ?? getTelemetryClient();
72
480
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
481
+ this.tracer = telemetry.getTracer('SequencerPublisher');
73
482
  this.l1TxUtils = deps.l1TxUtils;
483
+ this.getNextPublisher = deps.getNextPublisher;
74
484
  this.rollupContract = deps.rollupContract;
75
485
  this.govProposerContract = deps.governanceProposerContract;
76
486
  this.slashingProposerContract = deps.slashingProposerContract;
@@ -80,14 +490,49 @@ export class SequencerPublisher {
80
490
  this.slashingProposerContract = newSlashingProposer;
81
491
  });
82
492
  this.slashFactoryContract = deps.slashFactoryContract;
493
+ // Initialize L1 fee analyzer for fisherman mode
494
+ if (config.fishermanMode) {
495
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
496
+ }
497
+ // Initialize fee asset price oracle
498
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(this.l1TxUtils.client, this.rollupContract, createLogger('sequencer:publisher:price-oracle'));
499
+ // Initialize failed L1 tx store (optional, for test networks)
500
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
501
+ }
502
+ /**
503
+ * Backs up a failed L1 transaction to the configured store for debugging.
504
+ * Does nothing if no store is configured.
505
+ */ backupFailedTx(failedTx) {
506
+ if (!this.failedTxStore) {
507
+ return;
508
+ }
509
+ const tx = {
510
+ ...failedTx,
511
+ timestamp: Date.now()
512
+ };
513
+ // Fire and forget - don't block on backup
514
+ void this.failedTxStore.then((store)=>store?.saveFailedTx(tx)).catch((err)=>{
515
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
516
+ });
83
517
  }
84
518
  getRollupContract() {
85
519
  return this.rollupContract;
86
520
  }
521
+ /**
522
+ * Gets the fee asset price modifier from the oracle.
523
+ * Returns 0n if the oracle query fails.
524
+ */ getFeeAssetPriceModifier() {
525
+ return this.feeAssetPriceOracle.computePriceModifier();
526
+ }
87
527
  getSenderAddress() {
88
528
  return this.l1TxUtils.getSenderAddress();
89
529
  }
90
530
  /**
531
+ * Gets the L1 fee analyzer instance (only available in fisherman mode)
532
+ */ getL1FeeAnalyzer() {
533
+ return this.l1FeeAnalyzer;
534
+ }
535
+ /**
91
536
  * Sets the proposer address to use for simulations in fisherman mode.
92
537
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
93
538
  */ setProposerAddressForSimulation(proposerAddress) {
@@ -97,7 +542,7 @@ export class SequencerPublisher {
97
542
  this.requests.push(request);
98
543
  }
99
544
  getCurrentL2Slot() {
100
- return this.epochCache.getEpochAndSlotNow().slot;
545
+ return this.epochCache.getSlotNow();
101
546
  }
102
547
  /**
103
548
  * Clears all pending requests without sending them.
@@ -109,6 +554,46 @@ export class SequencerPublisher {
109
554
  }
110
555
  }
111
556
  /**
557
+ * Analyzes L1 fees for the pending requests without sending them.
558
+ * This is used in fisherman mode to validate fee calculations.
559
+ * @param l2SlotNumber - The L2 slot number for this analysis
560
+ * @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
561
+ * @returns The analysis result (incomplete until block mines), or undefined if no requests
562
+ */ async analyzeL1Fees(l2SlotNumber, onComplete) {
563
+ if (!this.l1FeeAnalyzer) {
564
+ this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
565
+ return undefined;
566
+ }
567
+ const requestsToAnalyze = [
568
+ ...this.requests
569
+ ];
570
+ if (requestsToAnalyze.length === 0) {
571
+ this.log.debug('No requests to analyze for L1 fees');
572
+ return undefined;
573
+ }
574
+ // Extract blob config from requests (if any)
575
+ const blobConfigs = requestsToAnalyze.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
576
+ const blobConfig = blobConfigs[0];
577
+ // Get gas configs
578
+ const gasConfigs = requestsToAnalyze.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
579
+ const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
580
+ const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g)=>sum + g, 0n) : 0n;
581
+ // Get the transaction requests
582
+ const l1Requests = requestsToAnalyze.map((r)=>r.request);
583
+ // Start the analysis
584
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT, l1Requests, blobConfig, onComplete);
585
+ this.log.info('Started L1 fee analysis', {
586
+ analysisId,
587
+ l2SlotNumber: l2SlotNumber.toString(),
588
+ requestCount: requestsToAnalyze.length,
589
+ hasBlobConfig: !!blobConfig,
590
+ gasLimit: gasLimit.toString(),
591
+ actions: requestsToAnalyze.map((r)=>r.action)
592
+ });
593
+ // Return the analysis result (will be incomplete until block mines)
594
+ return this.l1FeeAnalyzer.getAnalysis(analysisId);
595
+ }
596
+ /**
112
597
  * Sends all requests that are still valid.
113
598
  * @returns one of:
114
599
  * - A receipt and stats if the tx succeeded
@@ -119,7 +604,7 @@ export class SequencerPublisher {
119
604
  ...this.requests
120
605
  ];
121
606
  this.requests = [];
122
- if (this.interrupted) {
607
+ if (this.interrupted || requestsToProcess.length === 0) {
123
608
  return undefined;
124
609
  }
125
610
  const currentL2Slot = this.getCurrentL2Slot();
@@ -146,15 +631,24 @@ export class SequencerPublisher {
146
631
  // @note - we can only have one blob config per bundle
147
632
  // find requests with gas and blob configs
148
633
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
149
- const gasConfigs = requestsToProcess.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
150
- const blobConfigs = requestsToProcess.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
634
+ const gasConfigs = validRequests.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
635
+ const blobConfigs = validRequests.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
151
636
  if (blobConfigs.length > 1) {
152
637
  throw new Error('Multiple blob configs found');
153
638
  }
154
639
  const blobConfig = blobConfigs[0];
155
640
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
156
641
  const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
157
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
642
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
643
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
644
+ const maxGas = MAX_L1_TX_LIMIT;
645
+ if (gasLimit !== undefined && gasLimit > maxGas) {
646
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
647
+ requested: gasLimit,
648
+ capped: maxGas
649
+ });
650
+ gasLimit = maxGas;
651
+ }
158
652
  const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
159
653
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
160
654
  const txConfig = {
@@ -165,12 +659,34 @@ export class SequencerPublisher {
165
659
  // This ensures the committee gets precomputed correctly
166
660
  validRequests.sort((a, b)=>compareActions(a.action, b.action));
167
661
  try {
662
+ // Capture context for failed tx backup before sending
663
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
664
+ const multicallData = encodeFunctionData({
665
+ abi: multicall3Abi,
666
+ functionName: 'aggregate3',
667
+ args: [
668
+ validRequests.map((r)=>({
669
+ target: r.request.to,
670
+ callData: r.request.data,
671
+ allowFailure: true
672
+ }))
673
+ ]
674
+ });
675
+ const blobDataHex = blobConfig?.blobs?.map((b)=>toHex(b));
676
+ const txContext = {
677
+ multicallData,
678
+ blobData: blobDataHex,
679
+ l1BlockNumber
680
+ };
168
681
  this.log.debug('Forwarding transactions', {
169
682
  validRequests: validRequests.map((request)=>request.action),
170
683
  txConfig
171
684
  });
172
- const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils, txConfig, blobConfig, this.rollupContract.address, this.log);
173
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
685
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
686
+ if (result === undefined) {
687
+ return undefined;
688
+ }
689
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result, txContext);
174
690
  return {
175
691
  result,
176
692
  expiredActions,
@@ -190,17 +706,83 @@ export class SequencerPublisher {
190
706
  }
191
707
  }
192
708
  }
193
- callbackBundledTransactions(requests, result) {
709
+ /**
710
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
711
+ * failure occurs (i.e. the tx never reached the chain).
712
+ * On-chain reverts and simulation errors are returned as-is without rotation.
713
+ */ async forwardWithPublisherRotation(validRequests, txConfig, blobConfig) {
714
+ const triedAddresses = [];
715
+ let currentPublisher = this.l1TxUtils;
716
+ while(true){
717
+ triedAddresses.push(currentPublisher.getSenderAddress());
718
+ try {
719
+ const result = await Multicall3.forward(validRequests.map((r)=>r.request), currentPublisher, txConfig, blobConfig, this.rollupContract.address, this.log);
720
+ this.l1TxUtils = currentPublisher;
721
+ return result;
722
+ } catch (err) {
723
+ if (err instanceof TimeoutError) {
724
+ throw err;
725
+ }
726
+ const viemError = formatViemError(err);
727
+ if (!this.getNextPublisher) {
728
+ this.log.error('Failed to publish bundled transactions', viemError);
729
+ return undefined;
730
+ }
731
+ this.log.warn(`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`, viemError);
732
+ const nextPublisher = await this.getNextPublisher([
733
+ ...triedAddresses
734
+ ]);
735
+ if (!nextPublisher) {
736
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
737
+ return undefined;
738
+ }
739
+ currentPublisher = nextPublisher;
740
+ }
741
+ }
742
+ }
743
+ callbackBundledTransactions(requests, result, txContext) {
194
744
  const actionsListStr = requests.map((r)=>r.action).join(', ');
195
745
  if (result instanceof FormattedViemError) {
196
746
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
747
+ this.backupFailedTx({
748
+ id: keccak256(txContext.multicallData),
749
+ failureType: 'send-error',
750
+ request: {
751
+ to: MULTI_CALL_3_ADDRESS,
752
+ data: txContext.multicallData
753
+ },
754
+ blobData: txContext.blobData,
755
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
756
+ error: {
757
+ message: result.message,
758
+ name: result.name
759
+ },
760
+ context: {
761
+ actions: requests.map((r)=>r.action),
762
+ requests: requests.map((r)=>({
763
+ action: r.action,
764
+ to: r.request.to,
765
+ data: r.request.data
766
+ })),
767
+ sender: this.getSenderAddress().toString()
768
+ }
769
+ });
197
770
  return {
198
771
  failedActions: requests.map((r)=>r.action)
199
772
  };
200
773
  } else {
201
774
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
202
775
  result,
203
- requests
776
+ requests: requests.map((r)=>({
777
+ ...r,
778
+ // Avoid logging large blob data
779
+ blobConfig: r.blobConfig ? {
780
+ ...r.blobConfig,
781
+ blobs: r.blobConfig.blobs.map((b)=>({
782
+ size: trimmedBytesLength(b)
783
+ }))
784
+ } : undefined
785
+ }))
204
786
  });
205
787
  const successfulActions = [];
206
788
  const failedActions = [];
@@ -211,6 +793,37 @@ export class SequencerPublisher {
211
793
  failedActions.push(request.action);
212
794
  }
213
795
  }
796
+ // Single backup for the whole reverted tx
797
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
798
+ this.backupFailedTx({
799
+ id: result.receipt.transactionHash,
800
+ failureType: 'revert',
801
+ request: {
802
+ to: MULTI_CALL_3_ADDRESS,
803
+ data: txContext.multicallData
804
+ },
805
+ blobData: txContext.blobData,
806
+ l1BlockNumber: result.receipt.blockNumber.toString(),
807
+ receipt: {
808
+ transactionHash: result.receipt.transactionHash,
809
+ blockNumber: result.receipt.blockNumber.toString(),
810
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
811
+ status: 'reverted'
812
+ },
813
+ error: {
814
+ message: result.errorMsg ?? 'Transaction reverted'
815
+ },
816
+ context: {
817
+ actions: failedActions,
818
+ requests: requests.filter((r)=>failedActions.includes(r.action)).map((r)=>({
819
+ action: r.action,
820
+ to: r.request.to,
821
+ data: r.request.data
822
+ })),
823
+ sender: this.getSenderAddress().toString()
824
+ }
825
+ });
826
+ }
214
827
  return {
215
828
  successfulActions,
216
829
  failedActions
@@ -218,18 +831,21 @@ export class SequencerPublisher {
218
831
  }
219
832
  }
220
833
  /**
221
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
834
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
222
835
  * @param tipArchive - The archive to check
223
836
  * @returns The slot and block number if it is possible to propose, undefined otherwise
224
- */ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
837
+ */ async canProposeAt(tipArchive, msgSender, opts = {}) {
225
838
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
226
839
  const ignoredErrors = [
227
840
  'SlotAlreadyInChain',
228
841
  'InvalidProposer',
229
842
  'InvalidArchive'
230
843
  ];
231
- return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
232
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber
844
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
845
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
846
+ const nextL1SlotTs = await this.getNextL1SlotTimestampWithL1Floor() + slotOffset;
847
+ return this.rollupContract.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
848
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
233
849
  }).catch((err)=>{
234
850
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
235
851
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
@@ -257,11 +873,11 @@ export class SequencerPublisher {
257
873
  [],
258
874
  Signature.empty().toViemSignature(),
259
875
  `0x${'0'.repeat(64)}`,
260
- header.contentCommitment.blobsHash.toString(),
876
+ header.blobsHash.toString(),
261
877
  flags
262
878
  ];
263
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
264
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
879
+ const ts = await this.getNextL1SlotTimestampWithL1Floor();
880
+ const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingCheckpointNumber);
265
881
  let balance = 0n;
266
882
  if (this.config.fishermanMode) {
267
883
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -288,34 +904,38 @@ export class SequencerPublisher {
288
904
  this.log.debug(`Simulated validateHeader`);
289
905
  }
290
906
  /**
291
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
292
- * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
293
- */ async simulateInvalidateBlock(validationResult) {
907
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
908
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
909
+ */ async simulateInvalidateCheckpoint(validationResult) {
294
910
  if (validationResult.valid) {
295
911
  return undefined;
296
912
  }
297
- const { reason, block } = validationResult;
298
- const blockNumber = block.blockNumber;
913
+ const { reason, checkpoint } = validationResult;
914
+ const checkpointNumber = checkpoint.checkpointNumber;
299
915
  const logData = {
300
- ...block,
916
+ ...checkpoint,
301
917
  reason
302
918
  };
303
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
304
- if (currentBlockNumber < validationResult.block.blockNumber) {
305
- this.log.verbose(`Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`, {
306
- currentBlockNumber,
919
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
920
+ if (currentCheckpointNumber < checkpointNumber) {
921
+ this.log.verbose(`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`, {
922
+ currentCheckpointNumber,
307
923
  ...logData
308
924
  });
309
925
  return undefined;
310
926
  }
311
- const request = this.buildInvalidateBlockRequest(validationResult);
312
- this.log.debug(`Simulating invalidate block ${blockNumber}`, {
927
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
928
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, {
313
929
  ...logData,
314
930
  request
315
931
  });
932
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
316
933
  try {
317
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
318
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, {
934
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, mergeAbis([
935
+ request.abi ?? [],
936
+ ErrorsAbi
937
+ ]));
938
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
319
939
  ...logData,
320
940
  request,
321
941
  gasUsed
@@ -323,90 +943,94 @@ export class SequencerPublisher {
323
943
  return {
324
944
  request,
325
945
  gasUsed,
326
- blockNumber,
327
- forcePendingBlockNumber: blockNumber - 1,
946
+ checkpointNumber,
947
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
328
948
  reason
329
949
  };
330
950
  } catch (err) {
331
951
  const viemError = formatViemError(err);
332
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
333
- // we can safely ignore it and return undefined so we go ahead with block building.
334
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
335
- this.log.verbose(`Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`, {
952
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
953
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
954
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
955
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`, {
336
956
  ...logData,
337
957
  request,
338
958
  error: viemError.message
339
959
  });
340
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
341
- if (latestPendingBlockNumber < blockNumber) {
342
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, {
960
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
961
+ if (latestPendingCheckpointNumber < checkpointNumber) {
962
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, {
343
963
  ...logData
344
964
  });
345
965
  return undefined;
346
966
  } else {
347
- this.log.error(`Simulation for invalidate ${blockNumber} failed and it is still in pending chain`, viemError, logData);
348
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
967
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`, viemError, logData);
968
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`, {
349
969
  cause: viemError
350
970
  });
351
971
  }
352
972
  }
353
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
354
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
355
- throw new Error(`Failed to simulate invalidate block ${blockNumber}`, {
973
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
974
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
975
+ this.backupFailedTx({
976
+ id: keccak256(request.data),
977
+ failureType: 'simulation',
978
+ request: {
979
+ to: request.to,
980
+ data: request.data,
981
+ value: request.value?.toString()
982
+ },
983
+ l1BlockNumber: l1BlockNumber.toString(),
984
+ error: {
985
+ message: viemError.message,
986
+ name: viemError.name
987
+ },
988
+ context: {
989
+ actions: [
990
+ `invalidate-${reason}`
991
+ ],
992
+ checkpointNumber,
993
+ sender: this.getSenderAddress().toString()
994
+ }
995
+ });
996
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, {
356
997
  cause: viemError
357
998
  });
358
999
  }
359
1000
  }
360
- buildInvalidateBlockRequest(validationResult) {
1001
+ buildInvalidateCheckpointRequest(validationResult) {
361
1002
  if (validationResult.valid) {
362
- throw new Error('Cannot invalidate a valid block');
1003
+ throw new Error('Cannot invalidate a valid checkpoint');
363
1004
  }
364
- const { block, committee, reason } = validationResult;
1005
+ const { checkpoint, committee, reason } = validationResult;
365
1006
  const logData = {
366
- ...block,
1007
+ ...checkpoint,
367
1008
  reason
368
1009
  };
369
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
1010
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
370
1011
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(validationResult.attestations).getPackedAttestations();
371
1012
  if (reason === 'invalid-attestation') {
372
- return this.rollupContract.buildInvalidateBadAttestationRequest(block.blockNumber, attestationsAndSigners, committee, validationResult.invalidIndex);
1013
+ return this.rollupContract.buildInvalidateBadAttestationRequest(checkpoint.checkpointNumber, attestationsAndSigners, committee, validationResult.invalidIndex);
373
1014
  } else if (reason === 'insufficient-attestations') {
374
- return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(block.blockNumber, attestationsAndSigners, committee);
1015
+ return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(checkpoint.checkpointNumber, attestationsAndSigners, committee);
375
1016
  } else {
376
1017
  const _ = reason;
377
1018
  throw new Error(`Unknown reason for invalidation`);
378
1019
  }
379
1020
  }
380
- /**
381
- * @notice Will simulate `propose` to make sure that the block is valid for submission
382
- *
383
- * @dev Throws if unable to propose
384
- *
385
- * @param block - The block to propose
386
- * @param attestationData - The block's attestation data
387
- *
388
- */ async validateBlockForSubmission(block, attestationsAndSigners, attestationsAndSignersSignature, options) {
389
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
390
- // If we have no attestations, we still need to provide the empty attestations
391
- // so that the committee is recalculated correctly
392
- const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
393
- if (ignoreSignatures) {
394
- const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
395
- if (!committee) {
396
- this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
397
- throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
398
- }
399
- attestationsAndSigners.attestations = committee.map((committeeMember)=>CommitteeAttestation.fromAddress(committeeMember));
400
- }
401
- const blobFields = block.getCheckpointBlobFields();
402
- const blobs = getBlobsPerL1Block(blobFields);
1021
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
1022
+ // Anchor the simulation timestamp to the checkpoint's own slot start time
1023
+ // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
1024
+ const ts = checkpoint.header.timestamp;
1025
+ const blobFields = checkpoint.toBlobFields();
1026
+ const blobs = await getBlobsPerL1Block(blobFields);
403
1027
  const blobInput = getPrefixedEthBlobCommitments(blobs);
404
1028
  const args = [
405
1029
  {
406
- header: block.getCheckpointHeader().toViem(),
407
- archive: toHex(block.archive.root.toBuffer()),
1030
+ header: checkpoint.header.toViem(),
1031
+ archive: toHex(checkpoint.archive.root.toBuffer()),
408
1032
  oracleInput: {
409
- feeAssetPriceModifier: 0n
1033
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
410
1034
  }
411
1035
  },
412
1036
  attestationsAndSigners.getPackedAttestations(),
@@ -431,9 +1055,38 @@ export class SequencerPublisher {
431
1055
  }
432
1056
  const round = await base.computeRound(slotNumber);
433
1057
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
1058
+ if (roundInfo.quorumReached) {
1059
+ return false;
1060
+ }
434
1061
  if (roundInfo.lastSignalSlot >= slotNumber) {
435
1062
  return false;
436
1063
  }
1064
+ if (await this.isPayloadEmpty(payload)) {
1065
+ this.log.warn(`Skipping vote cast for payload with empty code`);
1066
+ return false;
1067
+ }
1068
+ // Check if payload was already submitted to governance
1069
+ const cacheKey = payload.toString();
1070
+ if (!this.payloadProposedCache.has(cacheKey)) {
1071
+ try {
1072
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
1073
+ const proposed = await retry(()=>base.hasPayloadBeenProposed(payload.toString(), l1StartBlock), 'Check if payload was proposed', makeBackoff([
1074
+ 0,
1075
+ 1,
1076
+ 2
1077
+ ]), this.log, true);
1078
+ if (proposed) {
1079
+ this.payloadProposedCache.add(cacheKey);
1080
+ }
1081
+ } catch (err) {
1082
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
1083
+ return false;
1084
+ }
1085
+ }
1086
+ if (this.payloadProposedCache.has(cacheKey)) {
1087
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
1088
+ return false;
1089
+ }
437
1090
  const cachedLastVote = this.lastActions[signalType];
438
1091
  this.lastActions[signalType] = slotNumber;
439
1092
  const action = signalType;
@@ -444,15 +1097,41 @@ export class SequencerPublisher {
444
1097
  signer: this.l1TxUtils.client.account?.address,
445
1098
  lastValidL2Slot: slotNumber
446
1099
  });
1100
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
447
1101
  try {
448
1102
  await this.l1TxUtils.simulate(request, {
449
1103
  time: timestamp
450
- }, [], ErrorsAbi);
1104
+ }, [], mergeAbis([
1105
+ request.abi ?? [],
1106
+ ErrorsAbi
1107
+ ]));
451
1108
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
452
1109
  request
453
1110
  });
454
1111
  } catch (err) {
455
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
1112
+ const viemError = formatViemError(err);
1113
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
1114
+ this.backupFailedTx({
1115
+ id: keccak256(request.data),
1116
+ failureType: 'simulation',
1117
+ request: {
1118
+ to: request.to,
1119
+ data: request.data,
1120
+ value: request.value?.toString()
1121
+ },
1122
+ l1BlockNumber: l1BlockNumber.toString(),
1123
+ error: {
1124
+ message: viemError.message,
1125
+ name: viemError.name
1126
+ },
1127
+ context: {
1128
+ actions: [
1129
+ action
1130
+ ],
1131
+ slot: slotNumber,
1132
+ sender: this.getSenderAddress().toString()
1133
+ }
1134
+ });
456
1135
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
457
1136
  }
458
1137
  // TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
@@ -472,17 +1151,27 @@ export class SequencerPublisher {
472
1151
  payload: payload.toString()
473
1152
  };
474
1153
  if (!success) {
475
- this.log.error(`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`, logData);
1154
+ this.log.error(`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`, logData);
476
1155
  this.lastActions[signalType] = cachedLastVote;
477
1156
  return false;
478
1157
  } else {
479
- this.log.info(`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`, logData);
1158
+ this.log.info(`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`, logData);
480
1159
  return true;
481
1160
  }
482
1161
  }
483
1162
  });
484
1163
  return true;
485
1164
  }
1165
+ async isPayloadEmpty(payload) {
1166
+ const key = payload.toString();
1167
+ const cached = this.isPayloadEmptyCache.get(key);
1168
+ if (cached) {
1169
+ return cached;
1170
+ }
1171
+ const isEmpty = !await this.l1TxUtils.getCode(payload);
1172
+ this.isPayloadEmptyCache.set(key, isEmpty);
1173
+ return isEmpty;
1174
+ }
486
1175
  /**
487
1176
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
488
1177
  * @param slotNumber - The slot number to cast a signal for.
@@ -578,22 +1267,17 @@ export class SequencerPublisher {
578
1267
  }
579
1268
  return true;
580
1269
  }
581
- /**
582
- * Proposes a L2 block on L1.
583
- *
584
- * @param block - L2 block to propose.
585
- * @returns True if the tx has been enqueued, throws otherwise. See #9315
586
- */ async enqueueProposeL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
587
- const checkpointHeader = block.getCheckpointHeader();
588
- const blobFields = block.getCheckpointBlobFields();
589
- const blobs = getBlobsPerL1Block(blobFields);
1270
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
1271
+ const checkpointHeader = checkpoint.header;
1272
+ const blobFields = checkpoint.toBlobFields();
1273
+ const blobs = await getBlobsPerL1Block(blobFields);
590
1274
  const proposeTxArgs = {
591
1275
  header: checkpointHeader,
592
- archive: block.archive.root.toBuffer(),
593
- body: block.body.toBuffer(),
1276
+ archive: checkpoint.archive.root.toBuffer(),
594
1277
  blobs,
595
1278
  attestationsAndSigners,
596
- attestationsAndSignersSignature
1279
+ attestationsAndSignersSignature,
1280
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier
597
1281
  };
598
1282
  let ts;
599
1283
  try {
@@ -602,36 +1286,35 @@ export class SequencerPublisher {
602
1286
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
603
1287
  // make time consistency checks break.
604
1288
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
605
- ts = await this.validateBlockForSubmission(block, attestationsAndSigners, attestationsAndSignersSignature, opts);
1289
+ ts = await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
606
1290
  } catch (err) {
607
- this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
608
- ...block.getStats(),
609
- slotNumber: block.header.globalVariables.slotNumber,
610
- forcePendingBlockNumber: opts.forcePendingBlockNumber
1291
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1292
+ ...checkpoint.getStats(),
1293
+ slotNumber: checkpoint.header.slotNumber,
1294
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber
611
1295
  });
612
1296
  throw err;
613
1297
  }
614
- this.log.verbose(`Enqueuing block propose transaction`, {
615
- ...block.toBlockInfo(),
1298
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1299
+ ...checkpoint.toCheckpointInfo(),
616
1300
  ...opts
617
1301
  });
618
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
619
- return true;
1302
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
620
1303
  }
621
- enqueueInvalidateBlock(request, opts = {}) {
1304
+ enqueueInvalidateCheckpoint(request, opts = {}) {
622
1305
  if (!request) {
623
1306
  return;
624
1307
  }
625
1308
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
626
1309
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(request.gasUsed) * 64 / 63)));
627
- const { gasUsed, blockNumber } = request;
1310
+ const { gasUsed, checkpointNumber } = request;
628
1311
  const logData = {
629
1312
  gasUsed,
630
- blockNumber,
1313
+ checkpointNumber,
631
1314
  gasLimit,
632
1315
  opts
633
1316
  };
634
- this.log.verbose(`Enqueuing invalidate block request`, logData);
1317
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
635
1318
  this.addRequest({
636
1319
  action: `invalidate-by-${request.reason}`,
637
1320
  request: request.request,
@@ -643,12 +1326,12 @@ export class SequencerPublisher {
643
1326
  checkSuccess: (_req, result)=>{
644
1327
  const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
645
1328
  if (!success) {
646
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, {
1329
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, {
647
1330
  ...result,
648
1331
  ...logData
649
1332
  });
650
1333
  } else {
651
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, {
1334
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, {
652
1335
  ...result,
653
1336
  ...logData
654
1337
  });
@@ -670,28 +1353,60 @@ export class SequencerPublisher {
670
1353
  const cachedLastActionSlot = this.lastActions[action];
671
1354
  this.lastActions[action] = slotNumber;
672
1355
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1356
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
673
1357
  let gasUsed;
1358
+ const simulateAbi = mergeAbis([
1359
+ request.abi ?? [],
1360
+ ErrorsAbi
1361
+ ]);
674
1362
  try {
675
1363
  ({ gasUsed } = await this.l1TxUtils.simulate(request, {
676
1364
  time: timestamp
677
- }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1365
+ }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
678
1366
  this.log.verbose(`Simulation for ${action} succeeded`, {
679
1367
  ...logData,
680
1368
  request,
681
1369
  gasUsed
682
1370
  });
683
1371
  } catch (err) {
684
- const viemError = formatViemError(err);
1372
+ const viemError = formatViemError(err, simulateAbi);
685
1373
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1374
+ this.backupFailedTx({
1375
+ id: keccak256(request.data),
1376
+ failureType: 'simulation',
1377
+ request: {
1378
+ to: request.to,
1379
+ data: request.data,
1380
+ value: request.value?.toString()
1381
+ },
1382
+ l1BlockNumber: l1BlockNumber.toString(),
1383
+ error: {
1384
+ message: viemError.message,
1385
+ name: viemError.name
1386
+ },
1387
+ context: {
1388
+ actions: [
1389
+ action
1390
+ ],
1391
+ slot: slotNumber,
1392
+ sender: this.getSenderAddress().toString()
1393
+ }
1394
+ });
686
1395
  return false;
687
1396
  }
688
1397
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
689
1398
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(gasUsed) * 64 / 63)));
690
1399
  logData.gasLimit = gasLimit;
1400
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1401
+ // when the tx is sent and a revert is diagnosed via simulation.
1402
+ const requestWithAbi = {
1403
+ ...request,
1404
+ abi: simulateAbi
1405
+ };
691
1406
  this.log.debug(`Enqueuing ${action}`, logData);
692
1407
  this.addRequest({
693
1408
  action,
694
- request,
1409
+ request: requestWithAbi,
695
1410
  gasConfig: {
696
1411
  gasLimit
697
1412
  },
@@ -755,10 +1470,38 @@ export class SequencerPublisher {
755
1470
  }, {}, {
756
1471
  blobs: encodedData.blobs.map((b)=>b.data),
757
1472
  kzg
758
- }).catch((err)=>{
759
- const { message, metaMessages } = formatViemError(err);
760
- this.log.error(`Failed to validate blobs`, message, {
761
- metaMessages
1473
+ }).catch(async (err)=>{
1474
+ const viemError = formatViemError(err);
1475
+ this.log.error(`Failed to validate blobs`, viemError.message, {
1476
+ metaMessages: viemError.metaMessages
1477
+ });
1478
+ const validateBlobsData = encodeFunctionData({
1479
+ abi: RollupAbi,
1480
+ functionName: 'validateBlobs',
1481
+ args: [
1482
+ blobInput
1483
+ ]
1484
+ });
1485
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1486
+ this.backupFailedTx({
1487
+ id: keccak256(validateBlobsData),
1488
+ failureType: 'simulation',
1489
+ request: {
1490
+ to: this.rollupContract.address,
1491
+ data: validateBlobsData
1492
+ },
1493
+ blobData: encodedData.blobs.map((b)=>toHex(b.data)),
1494
+ l1BlockNumber: l1BlockNumber.toString(),
1495
+ error: {
1496
+ message: viemError.message,
1497
+ name: viemError.name
1498
+ },
1499
+ context: {
1500
+ actions: [
1501
+ 'validate-blobs'
1502
+ ],
1503
+ sender: this.getSenderAddress().toString()
1504
+ }
762
1505
  });
763
1506
  throw new Error('Failed to validate blobs');
764
1507
  });
@@ -769,8 +1512,7 @@ export class SequencerPublisher {
769
1512
  header: encodedData.header.toViem(),
770
1513
  archive: toHex(encodedData.archive),
771
1514
  oracleInput: {
772
- // We are currently not modifying these. See #9963
773
- feeAssetPriceModifier: 0n
1515
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier
774
1516
  }
775
1517
  },
776
1518
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -797,8 +1539,8 @@ export class SequencerPublisher {
797
1539
  functionName: 'propose',
798
1540
  args
799
1541
  });
800
- // override the pending block number if requested
801
- const forcePendingBlockNumberStateDiff = (options.forcePendingBlockNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber) : []).flatMap((override)=>override.stateDiff ?? []);
1542
+ // override the pending checkpoint number if requested
1543
+ const forcePendingCheckpointNumberStateDiff = (options.forcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
802
1544
  const stateOverrides = [
803
1545
  {
804
1546
  address: this.rollupContract.address,
@@ -808,7 +1550,7 @@ export class SequencerPublisher {
808
1550
  slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
809
1551
  value: toPaddedHex(0n, true)
810
1552
  },
811
- ...forcePendingBlockNumberStateDiff
1553
+ ...forcePendingCheckpointNumberStateDiff
812
1554
  ]
813
1555
  }
814
1556
  ];
@@ -819,10 +1561,11 @@ export class SequencerPublisher {
819
1561
  balance: 10n * WEI_CONST * WEI_CONST
820
1562
  });
821
1563
  }
1564
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
822
1565
  const simulationResult = await this.l1TxUtils.simulate({
823
1566
  to: this.rollupContract.address,
824
1567
  data: rollupData,
825
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1568
+ gas: MAX_L1_TX_LIMIT,
826
1569
  ...this.proposerAddressForSimulation && {
827
1570
  from: this.proposerAddressForSimulation.toString()
828
1571
  }
@@ -830,10 +1573,10 @@ export class SequencerPublisher {
830
1573
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
831
1574
  time: timestamp + 1n,
832
1575
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
833
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n
1576
+ gasLimit: MAX_L1_TX_LIMIT * 2n
834
1577
  }, stateOverrides, RollupAbi, {
835
1578
  // @note fallback gas estimate to use if the node doesn't support simulation API
836
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS
1579
+ fallbackGasEstimate: MAX_L1_TX_LIMIT
837
1580
  }).catch((err)=>{
838
1581
  // In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
839
1582
  const viemError = formatViemError(err);
@@ -841,11 +1584,31 @@ export class SequencerPublisher {
841
1584
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
842
1585
  // Return a minimal simulation result with the fallback gas estimate
843
1586
  return {
844
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1587
+ gasUsed: MAX_L1_TX_LIMIT,
845
1588
  logs: []
846
1589
  };
847
1590
  }
848
1591
  this.log.error(`Failed to simulate propose tx`, viemError);
1592
+ this.backupFailedTx({
1593
+ id: keccak256(rollupData),
1594
+ failureType: 'simulation',
1595
+ request: {
1596
+ to: this.rollupContract.address,
1597
+ data: rollupData
1598
+ },
1599
+ l1BlockNumber: l1BlockNumber.toString(),
1600
+ error: {
1601
+ message: viemError.message,
1602
+ name: viemError.name
1603
+ },
1604
+ context: {
1605
+ actions: [
1606
+ 'propose'
1607
+ ],
1608
+ slot: Number(args[0].header.slotNumber),
1609
+ sender: this.getSenderAddress().toString()
1610
+ }
1611
+ });
849
1612
  throw err;
850
1613
  });
851
1614
  return {
@@ -853,24 +1616,25 @@ export class SequencerPublisher {
853
1616
  simulationResult
854
1617
  };
855
1618
  }
856
- async addProposeTx(block, encodedData, opts = {}, timestamp) {
1619
+ async addProposeTx(checkpoint, encodedData, opts = {}, timestamp) {
1620
+ const slot = checkpoint.header.slotNumber;
857
1621
  const timer = new Timer();
858
1622
  const kzg = Blob.getViemKzgInstance();
859
1623
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp, opts);
860
1624
  const startBlock = await this.l1TxUtils.getBlockNumber();
861
1625
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
862
- // Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
863
- // tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
864
- void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch((_err)=>{
865
- this.log.error('Failed to send blobs to blob sink');
866
- });
1626
+ // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1627
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1628
+ void Promise.resolve().then(()=>this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch((_err)=>{
1629
+ this.log.error('Failed to send blobs to blob client');
1630
+ }));
867
1631
  return this.addRequest({
868
1632
  action: 'propose',
869
1633
  request: {
870
1634
  to: this.rollupContract.address,
871
1635
  data: rollupData
872
1636
  },
873
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1637
+ lastValidL2Slot: checkpoint.header.slotNumber,
874
1638
  gasConfig: {
875
1639
  ...opts,
876
1640
  gasLimit
@@ -898,29 +1662,42 @@ export class SequencerPublisher {
898
1662
  calldataGas,
899
1663
  calldataSize,
900
1664
  sender,
901
- ...block.getStats(),
1665
+ ...checkpoint.getStats(),
902
1666
  eventName: 'rollup-published-to-l1',
903
1667
  blobCount: encodedData.blobs.length,
904
1668
  inclusionBlocks
905
1669
  };
906
- this.log.info(`Published L2 block to L1 rollup contract`, {
1670
+ this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
907
1671
  ...stats,
908
- ...block.getStats(),
909
- ...receipt
1672
+ ...checkpoint.getStats(),
1673
+ ...pick(receipt, 'transactionHash', 'blockHash')
910
1674
  });
911
1675
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
912
1676
  return true;
913
1677
  } else {
914
1678
  this.metrics.recordFailedTx('process');
915
- this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
916
- ...block.getStats(),
917
- receipt,
918
- txHash: receipt.transactionHash,
919
- slotNumber: block.header.globalVariables.slotNumber
1679
+ this.log.error(`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`, undefined, {
1680
+ ...checkpoint.getStats(),
1681
+ ...receipt
920
1682
  });
921
1683
  return false;
922
1684
  }
923
1685
  }
924
1686
  });
925
1687
  }
1688
+ /**
1689
+ * Returns the timestamp to use when simulating L1 proposal calls.
1690
+ * Uses the wall-clock-based next L1 slot boundary, but floors it with the latest L1 block timestamp
1691
+ * plus one slot duration. This prevents the sequencer from targeting a future L2 slot when the L1
1692
+ * chain hasn't caught up to the wall clock yet (e.g., the dateProvider is one L1 slot ahead of the
1693
+ * latest mined block), which would cause the propose tx to land in an L1 block with block.timestamp
1694
+ * still in the previous L2 slot.
1695
+ * TODO(palla): Properly fix by keeping dateProvider synced with anvil's chain time on every block.
1696
+ */ async getNextL1SlotTimestampWithL1Floor() {
1697
+ const l1Constants = this.epochCache.getL1Constants();
1698
+ const fromWallClock = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1699
+ const latestBlock = await this.l1TxUtils.client.getBlock();
1700
+ const fromL1Block = latestBlock.timestamp + BigInt(l1Constants.ethereumSlotDuration);
1701
+ return fromWallClock > fromL1Block ? fromWallClock : fromL1Block;
1702
+ }
926
1703
  }