@aztec/sequencer-client 4.0.0-nightly.20250907 → 4.0.0-nightly.20260107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/index.d.ts +1 -1
- package/dest/client/sequencer-client.d.ts +10 -8
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +40 -28
- package/dest/config.d.ts +13 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +82 -25
- package/dest/global_variable_builder/global_builder.d.ts +19 -13
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +41 -28
- package/dest/global_variable_builder/index.d.ts +1 -1
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +11 -8
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +21 -13
- package/dest/publisher/index.d.ts +2 -2
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +9 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +4 -4
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +76 -69
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +290 -180
- package/dest/sequencer/block_builder.d.ts +6 -10
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +21 -10
- package/dest/sequencer/checkpoint_builder.d.ts +63 -0
- package/dest/sequencer/checkpoint_builder.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_builder.js +131 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +74 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job.js +642 -0
- package/dest/sequencer/checkpoint_voter.d.ts +34 -0
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_voter.js +85 -0
- package/dest/sequencer/config.d.ts +3 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +11 -0
- package/dest/sequencer/errors.d.ts.map +1 -0
- package/dest/sequencer/errors.js +15 -0
- package/dest/sequencer/events.d.ts +46 -0
- package/dest/sequencer/events.d.ts.map +1 -0
- package/dest/sequencer/events.js +1 -0
- package/dest/sequencer/index.d.ts +5 -1
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +4 -0
- package/dest/sequencer/metrics.d.ts +37 -20
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +211 -85
- package/dest/sequencer/sequencer.d.ts +109 -121
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +798 -525
- package/dest/sequencer/timetable.d.ts +57 -21
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +150 -68
- package/dest/sequencer/types.d.ts +3 -0
- package/dest/sequencer/types.d.ts.map +1 -0
- package/dest/sequencer/types.js +1 -0
- package/dest/sequencer/utils.d.ts +20 -28
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +12 -24
- package/dest/test/index.d.ts +4 -2
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +83 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +179 -0
- package/dest/test/utils.d.ts +49 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +94 -0
- package/dest/tx_validator/nullifier_cache.d.ts +1 -1
- package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +12 -9
- package/package.json +32 -31
- package/src/client/sequencer-client.ts +34 -40
- package/src/config.ts +89 -29
- package/src/global_variable_builder/global_builder.ts +56 -48
- package/src/index.ts +2 -0
- package/src/publisher/config.ts +32 -19
- package/src/publisher/index.ts +1 -1
- package/src/publisher/sequencer-publisher-factory.ts +19 -6
- package/src/publisher/sequencer-publisher-metrics.ts +3 -3
- package/src/publisher/sequencer-publisher.ts +410 -240
- package/src/sequencer/README.md +531 -0
- package/src/sequencer/block_builder.ts +28 -30
- package/src/sequencer/checkpoint_builder.ts +217 -0
- package/src/sequencer/checkpoint_proposal_job.ts +706 -0
- package/src/sequencer/checkpoint_voter.ts +105 -0
- package/src/sequencer/config.ts +2 -1
- package/src/sequencer/errors.ts +21 -0
- package/src/sequencer/events.ts +27 -0
- package/src/sequencer/index.ts +4 -0
- package/src/sequencer/metrics.ts +269 -94
- package/src/sequencer/sequencer.ts +506 -676
- package/src/sequencer/timetable.ts +181 -91
- package/src/sequencer/types.ts +6 -0
- package/src/sequencer/utils.ts +24 -29
- package/src/test/index.ts +3 -1
- package/src/test/mock_checkpoint_builder.ts +247 -0
- package/src/test/utils.ts +137 -0
- package/src/tx_validator/tx_validator_factory.ts +13 -7
|
@@ -1,47 +1,48 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { Blob } from '@aztec/blob-lib';
|
|
3
|
-
import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
|
|
1
|
+
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
|
+
import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
4
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
+
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
-
FormattedViemError,
|
|
8
|
-
type GasPrice,
|
|
9
7
|
type GovernanceProposerContract,
|
|
10
8
|
type IEmpireBase,
|
|
11
|
-
type L1BlobInputs,
|
|
12
|
-
type L1ContractsConfig,
|
|
13
|
-
type L1GasConfig,
|
|
14
|
-
type L1TxRequest,
|
|
15
9
|
MULTI_CALL_3_ADDRESS,
|
|
16
10
|
Multicall3,
|
|
17
11
|
RollupContract,
|
|
18
12
|
type TallySlashingProposerContract,
|
|
19
|
-
type TransactionStats,
|
|
20
13
|
type ViemCommitteeAttestations,
|
|
21
14
|
type ViemHeader,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
} from '@aztec/ethereum/contracts';
|
|
16
|
+
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
17
|
+
import {
|
|
18
|
+
type L1BlobInputs,
|
|
19
|
+
type L1TxConfig,
|
|
20
|
+
type L1TxRequest,
|
|
21
|
+
type TransactionStats,
|
|
22
|
+
WEI_CONST,
|
|
23
|
+
} from '@aztec/ethereum/l1-tx-utils';
|
|
26
24
|
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
25
|
+
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
27
26
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
28
27
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
28
|
+
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
29
|
+
import { pick } from '@aztec/foundation/collection';
|
|
30
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
29
31
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
30
|
-
import type
|
|
31
|
-
import { createLogger } from '@aztec/foundation/log';
|
|
32
|
+
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
33
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
32
34
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
33
35
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
34
36
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
35
37
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
36
|
-
import {
|
|
38
|
+
import { CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
|
|
39
|
+
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
37
40
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
38
|
-
import {
|
|
39
|
-
import type {
|
|
40
|
-
import { type ProposedBlockHeader, StateReference, TxHash } from '@aztec/stdlib/tx';
|
|
41
|
+
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
42
|
+
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
41
43
|
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
42
44
|
|
|
43
|
-
import
|
|
44
|
-
import { type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
45
|
+
import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
45
46
|
|
|
46
47
|
import type { PublisherConfig, TxSenderConfig } from './config.js';
|
|
47
48
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
@@ -49,24 +50,17 @@ import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
|
49
50
|
/** Arguments to the process method of the rollup contract */
|
|
50
51
|
type L1ProcessArgs = {
|
|
51
52
|
/** The L2 block header. */
|
|
52
|
-
header:
|
|
53
|
+
header: CheckpointHeader;
|
|
53
54
|
/** A root of the archive tree after the L2 block is applied. */
|
|
54
55
|
archive: Buffer;
|
|
55
|
-
/** State reference after the L2 block is applied. */
|
|
56
|
-
stateReference: StateReference;
|
|
57
56
|
/** L2 block blobs containing all tx effects. */
|
|
58
57
|
blobs: Blob[];
|
|
59
|
-
/** L2 block tx hashes */
|
|
60
|
-
txHashes: TxHash[];
|
|
61
58
|
/** Attestations */
|
|
62
|
-
|
|
59
|
+
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
60
|
+
/** Attestations and signers signature */
|
|
61
|
+
attestationsAndSignersSignature: Signature;
|
|
63
62
|
};
|
|
64
63
|
|
|
65
|
-
export enum SignalType {
|
|
66
|
-
GOVERNANCE,
|
|
67
|
-
SLASHING,
|
|
68
|
-
}
|
|
69
|
-
|
|
70
64
|
export const Actions = [
|
|
71
65
|
'invalidate-by-invalid-attestation',
|
|
72
66
|
'invalidate-by-insufficient-attestations',
|
|
@@ -78,8 +72,11 @@ export const Actions = [
|
|
|
78
72
|
'vote-offenses',
|
|
79
73
|
'execute-slash',
|
|
80
74
|
] as const;
|
|
75
|
+
|
|
81
76
|
export type Action = (typeof Actions)[number];
|
|
82
77
|
|
|
78
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
|
|
79
|
+
|
|
83
80
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
84
81
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
85
82
|
|
|
@@ -87,19 +84,19 @@ export type InvalidateBlockRequest = {
|
|
|
87
84
|
request: L1TxRequest;
|
|
88
85
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
89
86
|
gasUsed: bigint;
|
|
90
|
-
blockNumber:
|
|
91
|
-
forcePendingBlockNumber:
|
|
87
|
+
blockNumber: BlockNumber;
|
|
88
|
+
forcePendingBlockNumber: BlockNumber;
|
|
92
89
|
};
|
|
93
90
|
|
|
94
91
|
interface RequestWithExpiry {
|
|
95
92
|
action: Action;
|
|
96
93
|
request: L1TxRequest;
|
|
97
|
-
lastValidL2Slot:
|
|
98
|
-
gasConfig?: Pick<
|
|
94
|
+
lastValidL2Slot: SlotNumber;
|
|
95
|
+
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
99
96
|
blobConfig?: L1BlobInputs;
|
|
100
97
|
checkSuccess: (
|
|
101
98
|
request: L1TxRequest,
|
|
102
|
-
result?: { receipt: TransactionReceipt;
|
|
99
|
+
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
103
100
|
) => boolean;
|
|
104
101
|
}
|
|
105
102
|
|
|
@@ -111,15 +108,20 @@ export class SequencerPublisher {
|
|
|
111
108
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
112
109
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
};
|
|
111
|
+
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
112
|
+
|
|
113
|
+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
118
114
|
|
|
119
|
-
protected log
|
|
115
|
+
protected log: Logger;
|
|
120
116
|
protected ethereumSlotDuration: bigint;
|
|
121
117
|
|
|
122
|
-
private
|
|
118
|
+
private blobClient: BlobClientInterface;
|
|
119
|
+
|
|
120
|
+
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
121
|
+
private proposerAddressForSimulation?: EthAddress;
|
|
122
|
+
|
|
123
|
+
/** L1 fee analyzer for fisherman mode */
|
|
124
|
+
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
123
125
|
// @note - with blobs, the below estimate seems too large.
|
|
124
126
|
// Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
|
|
125
127
|
// Total used for emptier block from above test: 429k (of which 84k is 1x blob)
|
|
@@ -143,7 +145,7 @@ export class SequencerPublisher {
|
|
|
143
145
|
private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
|
|
144
146
|
deps: {
|
|
145
147
|
telemetry?: TelemetryClient;
|
|
146
|
-
|
|
148
|
+
blobClient: BlobClientInterface;
|
|
147
149
|
l1TxUtils: L1TxUtilsWithBlobs;
|
|
148
150
|
rollupContract: RollupContract;
|
|
149
151
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -152,13 +154,16 @@ export class SequencerPublisher {
|
|
|
152
154
|
epochCache: EpochCache;
|
|
153
155
|
dateProvider: DateProvider;
|
|
154
156
|
metrics: SequencerPublisherMetrics;
|
|
157
|
+
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
158
|
+
log?: Logger;
|
|
155
159
|
},
|
|
156
160
|
) {
|
|
161
|
+
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
157
162
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
158
163
|
this.epochCache = deps.epochCache;
|
|
164
|
+
this.lastActions = deps.lastActions;
|
|
159
165
|
|
|
160
|
-
this.
|
|
161
|
-
deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
|
|
166
|
+
this.blobClient = deps.blobClient;
|
|
162
167
|
|
|
163
168
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
164
169
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
@@ -175,6 +180,15 @@ export class SequencerPublisher {
|
|
|
175
180
|
this.slashingProposerContract = newSlashingProposer;
|
|
176
181
|
});
|
|
177
182
|
this.slashFactoryContract = deps.slashFactoryContract;
|
|
183
|
+
|
|
184
|
+
// Initialize L1 fee analyzer for fisherman mode
|
|
185
|
+
if (config.fishermanMode) {
|
|
186
|
+
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
187
|
+
this.l1TxUtils.client,
|
|
188
|
+
deps.dateProvider,
|
|
189
|
+
createLogger('sequencer:publisher:fee-analyzer'),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
public getRollupContract(): RollupContract {
|
|
@@ -185,14 +199,96 @@ export class SequencerPublisher {
|
|
|
185
199
|
return this.l1TxUtils.getSenderAddress();
|
|
186
200
|
}
|
|
187
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Gets the L1 fee analyzer instance (only available in fisherman mode)
|
|
204
|
+
*/
|
|
205
|
+
public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
|
|
206
|
+
return this.l1FeeAnalyzer;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Sets the proposer address to use for simulations in fisherman mode.
|
|
211
|
+
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
212
|
+
*/
|
|
213
|
+
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
214
|
+
this.proposerAddressForSimulation = proposerAddress;
|
|
215
|
+
}
|
|
216
|
+
|
|
188
217
|
public addRequest(request: RequestWithExpiry) {
|
|
189
218
|
this.requests.push(request);
|
|
190
219
|
}
|
|
191
220
|
|
|
192
|
-
public getCurrentL2Slot():
|
|
221
|
+
public getCurrentL2Slot(): SlotNumber {
|
|
193
222
|
return this.epochCache.getEpochAndSlotNow().slot;
|
|
194
223
|
}
|
|
195
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Clears all pending requests without sending them.
|
|
227
|
+
*/
|
|
228
|
+
public clearPendingRequests(): void {
|
|
229
|
+
const count = this.requests.length;
|
|
230
|
+
this.requests = [];
|
|
231
|
+
if (count > 0) {
|
|
232
|
+
this.log.debug(`Cleared ${count} pending request(s)`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Analyzes L1 fees for the pending requests without sending them.
|
|
238
|
+
* This is used in fisherman mode to validate fee calculations.
|
|
239
|
+
* @param l2SlotNumber - The L2 slot number for this analysis
|
|
240
|
+
* @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
|
|
241
|
+
* @returns The analysis result (incomplete until block mines), or undefined if no requests
|
|
242
|
+
*/
|
|
243
|
+
public async analyzeL1Fees(
|
|
244
|
+
l2SlotNumber: SlotNumber,
|
|
245
|
+
onComplete?: (analysis: L1FeeAnalysisResult) => void,
|
|
246
|
+
): Promise<L1FeeAnalysisResult | undefined> {
|
|
247
|
+
if (!this.l1FeeAnalyzer) {
|
|
248
|
+
this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const requestsToAnalyze = [...this.requests];
|
|
253
|
+
if (requestsToAnalyze.length === 0) {
|
|
254
|
+
this.log.debug('No requests to analyze for L1 fees');
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Extract blob config from requests (if any)
|
|
259
|
+
const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
260
|
+
const blobConfig = blobConfigs[0];
|
|
261
|
+
|
|
262
|
+
// Get gas configs
|
|
263
|
+
const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
264
|
+
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
265
|
+
const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
|
|
266
|
+
|
|
267
|
+
// Get the transaction requests
|
|
268
|
+
const l1Requests = requestsToAnalyze.map(r => r.request);
|
|
269
|
+
|
|
270
|
+
// Start the analysis
|
|
271
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
272
|
+
l2SlotNumber,
|
|
273
|
+
gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
274
|
+
l1Requests,
|
|
275
|
+
blobConfig,
|
|
276
|
+
onComplete,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
this.log.info('Started L1 fee analysis', {
|
|
280
|
+
analysisId,
|
|
281
|
+
l2SlotNumber: l2SlotNumber.toString(),
|
|
282
|
+
requestCount: requestsToAnalyze.length,
|
|
283
|
+
hasBlobConfig: !!blobConfig,
|
|
284
|
+
gasLimit: gasLimit.toString(),
|
|
285
|
+
actions: requestsToAnalyze.map(r => r.action),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Return the analysis result (will be incomplete until block mines)
|
|
289
|
+
return this.l1FeeAnalyzer.getAnalysis(analysisId);
|
|
290
|
+
}
|
|
291
|
+
|
|
196
292
|
/**
|
|
197
293
|
* Sends all requests that are still valid.
|
|
198
294
|
* @returns one of:
|
|
@@ -203,7 +299,7 @@ export class SequencerPublisher {
|
|
|
203
299
|
public async sendRequests() {
|
|
204
300
|
const requestsToProcess = [...this.requests];
|
|
205
301
|
this.requests = [];
|
|
206
|
-
if (this.interrupted) {
|
|
302
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
207
303
|
return undefined;
|
|
208
304
|
}
|
|
209
305
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -249,18 +345,21 @@ export class SequencerPublisher {
|
|
|
249
345
|
const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
250
346
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
251
347
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
252
|
-
const
|
|
348
|
+
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
253
349
|
|
|
254
350
|
// Sort the requests so that proposals always go first
|
|
255
351
|
// This ensures the committee gets precomputed correctly
|
|
256
352
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
257
353
|
|
|
258
354
|
try {
|
|
259
|
-
this.log.debug('Forwarding transactions', {
|
|
355
|
+
this.log.debug('Forwarding transactions', {
|
|
356
|
+
validRequests: validRequests.map(request => request.action),
|
|
357
|
+
txConfig,
|
|
358
|
+
});
|
|
260
359
|
const result = await Multicall3.forward(
|
|
261
360
|
validRequests.map(request => request.request),
|
|
262
361
|
this.l1TxUtils,
|
|
263
|
-
|
|
362
|
+
txConfig,
|
|
264
363
|
blobConfig,
|
|
265
364
|
this.rollupContract.address,
|
|
266
365
|
this.log,
|
|
@@ -285,7 +384,7 @@ export class SequencerPublisher {
|
|
|
285
384
|
|
|
286
385
|
private callbackBundledTransactions(
|
|
287
386
|
requests: RequestWithExpiry[],
|
|
288
|
-
result?: { receipt: TransactionReceipt
|
|
387
|
+
result?: { receipt: TransactionReceipt } | FormattedViemError,
|
|
289
388
|
) {
|
|
290
389
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
291
390
|
if (result instanceof FormattedViemError) {
|
|
@@ -314,13 +413,18 @@ export class SequencerPublisher {
|
|
|
314
413
|
public canProposeAtNextEthBlock(
|
|
315
414
|
tipArchive: Fr,
|
|
316
415
|
msgSender: EthAddress,
|
|
317
|
-
opts: { forcePendingBlockNumber?:
|
|
416
|
+
opts: { forcePendingBlockNumber?: BlockNumber } = {},
|
|
318
417
|
) {
|
|
319
418
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
320
419
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
321
420
|
|
|
322
421
|
return this.rollupContract
|
|
323
|
-
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration,
|
|
422
|
+
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
|
|
423
|
+
forcePendingCheckpointNumber:
|
|
424
|
+
opts.forcePendingBlockNumber !== undefined
|
|
425
|
+
? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
|
|
426
|
+
: undefined,
|
|
427
|
+
})
|
|
324
428
|
.catch(err => {
|
|
325
429
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
326
430
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -339,25 +443,42 @@ export class SequencerPublisher {
|
|
|
339
443
|
* @param header - The block header to validate
|
|
340
444
|
*/
|
|
341
445
|
public async validateBlockHeader(
|
|
342
|
-
header:
|
|
343
|
-
opts?: { forcePendingBlockNumber:
|
|
446
|
+
header: CheckpointHeader,
|
|
447
|
+
opts?: { forcePendingBlockNumber: BlockNumber | undefined },
|
|
344
448
|
) {
|
|
345
449
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
346
450
|
|
|
347
451
|
const args = [
|
|
348
452
|
header.toViem(),
|
|
349
|
-
|
|
453
|
+
CommitteeAttestationsAndSigners.empty().getPackedAttestations(),
|
|
350
454
|
[], // no signers
|
|
455
|
+
Signature.empty().toViemSignature(),
|
|
351
456
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
352
457
|
header.contentCommitment.blobsHash.toString(),
|
|
353
458
|
flags,
|
|
354
459
|
] as const;
|
|
355
460
|
|
|
356
461
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
462
|
+
const optsForcePendingCheckpointNumber =
|
|
463
|
+
opts?.forcePendingBlockNumber !== undefined
|
|
464
|
+
? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
|
|
465
|
+
: undefined;
|
|
466
|
+
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
467
|
+
optsForcePendingCheckpointNumber,
|
|
468
|
+
);
|
|
469
|
+
let balance = 0n;
|
|
470
|
+
if (this.config.fishermanMode) {
|
|
471
|
+
// In fisherman mode, we can't know where the proposer is publishing from
|
|
472
|
+
// so we just add sufficient balance to the multicall3 address
|
|
473
|
+
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
474
|
+
} else {
|
|
475
|
+
balance = await this.l1TxUtils.getSenderBalance();
|
|
476
|
+
}
|
|
477
|
+
stateOverrides.push({
|
|
478
|
+
address: MULTI_CALL_3_ADDRESS,
|
|
479
|
+
balance,
|
|
480
|
+
});
|
|
357
481
|
|
|
358
|
-
// use sender balance to simulate
|
|
359
|
-
const balance = await this.l1TxUtils.getSenderBalance();
|
|
360
|
-
this.log.debug(`Simulating validateHeader with balance: ${balance}`);
|
|
361
482
|
await this.l1TxUtils.simulate(
|
|
362
483
|
{
|
|
363
484
|
to: this.rollupContract.address,
|
|
@@ -365,10 +486,7 @@ export class SequencerPublisher {
|
|
|
365
486
|
from: MULTI_CALL_3_ADDRESS,
|
|
366
487
|
},
|
|
367
488
|
{ time: ts + 1n },
|
|
368
|
-
|
|
369
|
-
{ address: MULTI_CALL_3_ADDRESS, balance },
|
|
370
|
-
...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
|
|
371
|
-
],
|
|
489
|
+
stateOverrides,
|
|
372
490
|
);
|
|
373
491
|
this.log.debug(`Simulated validateHeader`);
|
|
374
492
|
}
|
|
@@ -385,11 +503,11 @@ export class SequencerPublisher {
|
|
|
385
503
|
}
|
|
386
504
|
|
|
387
505
|
const { reason, block } = validationResult;
|
|
388
|
-
const blockNumber = block.
|
|
389
|
-
const logData = { ...block
|
|
506
|
+
const blockNumber = block.blockNumber;
|
|
507
|
+
const logData = { ...block, reason };
|
|
390
508
|
|
|
391
|
-
const currentBlockNumber = await this.rollupContract.
|
|
392
|
-
if (currentBlockNumber < validationResult.block.
|
|
509
|
+
const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
|
|
510
|
+
if (currentBlockNumber < validationResult.block.blockNumber) {
|
|
393
511
|
this.log.verbose(
|
|
394
512
|
`Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
|
|
395
513
|
{ currentBlockNumber, ...logData },
|
|
@@ -398,13 +516,13 @@ export class SequencerPublisher {
|
|
|
398
516
|
}
|
|
399
517
|
|
|
400
518
|
const request = this.buildInvalidateBlockRequest(validationResult);
|
|
401
|
-
this.log.debug(`Simulating invalidate block ${blockNumber}`, logData);
|
|
519
|
+
this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
|
|
402
520
|
|
|
403
521
|
try {
|
|
404
522
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
|
|
405
523
|
this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
|
|
406
524
|
|
|
407
|
-
return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
|
|
525
|
+
return { request, gasUsed, blockNumber, forcePendingBlockNumber: BlockNumber(blockNumber - 1), reason };
|
|
408
526
|
} catch (err) {
|
|
409
527
|
const viemError = formatViemError(err);
|
|
410
528
|
|
|
@@ -415,7 +533,7 @@ export class SequencerPublisher {
|
|
|
415
533
|
`Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
|
|
416
534
|
{ ...logData, request, error: viemError.message },
|
|
417
535
|
);
|
|
418
|
-
const latestPendingBlockNumber = await this.rollupContract.
|
|
536
|
+
const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
|
|
419
537
|
if (latestPendingBlockNumber < blockNumber) {
|
|
420
538
|
this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
|
|
421
539
|
return undefined;
|
|
@@ -443,20 +561,24 @@ export class SequencerPublisher {
|
|
|
443
561
|
}
|
|
444
562
|
|
|
445
563
|
const { block, committee, reason } = validationResult;
|
|
446
|
-
const logData = { ...block
|
|
447
|
-
this.log.debug(`Simulating invalidate block ${block.
|
|
564
|
+
const logData = { ...block, reason };
|
|
565
|
+
this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
|
|
566
|
+
|
|
567
|
+
const attestationsAndSigners = new CommitteeAttestationsAndSigners(
|
|
568
|
+
validationResult.attestations,
|
|
569
|
+
).getPackedAttestations();
|
|
448
570
|
|
|
449
571
|
if (reason === 'invalid-attestation') {
|
|
450
572
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
451
|
-
|
|
452
|
-
|
|
573
|
+
CheckpointNumber.fromBlockNumber(block.blockNumber),
|
|
574
|
+
attestationsAndSigners,
|
|
453
575
|
committee,
|
|
454
576
|
validationResult.invalidIndex,
|
|
455
577
|
);
|
|
456
578
|
} else if (reason === 'insufficient-attestations') {
|
|
457
579
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
458
|
-
|
|
459
|
-
|
|
580
|
+
CheckpointNumber.fromBlockNumber(block.blockNumber),
|
|
581
|
+
attestationsAndSigners,
|
|
460
582
|
committee,
|
|
461
583
|
);
|
|
462
584
|
} else {
|
|
@@ -465,59 +587,45 @@ export class SequencerPublisher {
|
|
|
465
587
|
}
|
|
466
588
|
}
|
|
467
589
|
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
* @param attestationData - The block's attestation data
|
|
475
|
-
*
|
|
476
|
-
*/
|
|
477
|
-
public async validateBlockForSubmission(
|
|
478
|
-
block: L2Block,
|
|
479
|
-
attestationData: { digest: Buffer; attestations: CommitteeAttestation[] } = {
|
|
480
|
-
digest: Buffer.alloc(32),
|
|
481
|
-
attestations: [],
|
|
482
|
-
},
|
|
483
|
-
options: { forcePendingBlockNumber?: number },
|
|
590
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
591
|
+
public async validateCheckpointForSubmission(
|
|
592
|
+
checkpoint: Checkpoint,
|
|
593
|
+
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
594
|
+
attestationsAndSignersSignature: Signature,
|
|
595
|
+
options: { forcePendingBlockNumber?: BlockNumber }, // TODO(palla/mbps): Should this be forcePendingCheckpointNumber?
|
|
484
596
|
): Promise<bigint> {
|
|
485
597
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
486
598
|
|
|
599
|
+
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
487
600
|
// If we have no attestations, we still need to provide the empty attestations
|
|
488
601
|
// so that the committee is recalculated correctly
|
|
489
|
-
const ignoreSignatures =
|
|
490
|
-
if (ignoreSignatures) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
const formattedAttestations = attestationData.attestations.map(attest => attest.toViem());
|
|
505
|
-
const signers = attestationData.attestations
|
|
506
|
-
.filter(attest => !attest.signature.isEmpty())
|
|
507
|
-
.map(attest => attest.address.toString());
|
|
602
|
+
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
603
|
+
// if (ignoreSignatures) {
|
|
604
|
+
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
605
|
+
// if (!committee) {
|
|
606
|
+
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
607
|
+
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
608
|
+
// }
|
|
609
|
+
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
610
|
+
// CommitteeAttestation.fromAddress(committeeMember),
|
|
611
|
+
// );
|
|
612
|
+
// }
|
|
613
|
+
|
|
614
|
+
const blobFields = checkpoint.toBlobFields();
|
|
615
|
+
const blobs = getBlobsPerL1Block(blobFields);
|
|
616
|
+
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
508
617
|
|
|
509
618
|
const args = [
|
|
510
619
|
{
|
|
511
|
-
header:
|
|
512
|
-
archive: toHex(
|
|
513
|
-
stateReference: block.header.state.toViem(),
|
|
514
|
-
txHashes: block.body.txEffects.map(txEffect => txEffect.txHash.toString()),
|
|
620
|
+
header: checkpoint.header.toViem(),
|
|
621
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
515
622
|
oracleInput: {
|
|
516
623
|
feeAssetPriceModifier: 0n,
|
|
517
624
|
},
|
|
518
625
|
},
|
|
519
|
-
|
|
520
|
-
|
|
626
|
+
attestationsAndSigners.getPackedAttestations(),
|
|
627
|
+
attestationsAndSigners.getSigners().map(signer => signer.toString()),
|
|
628
|
+
attestationsAndSignersSignature.toViemSignature(),
|
|
521
629
|
blobInput,
|
|
522
630
|
] as const;
|
|
523
631
|
|
|
@@ -526,15 +634,16 @@ export class SequencerPublisher {
|
|
|
526
634
|
}
|
|
527
635
|
|
|
528
636
|
private async enqueueCastSignalHelper(
|
|
529
|
-
slotNumber:
|
|
637
|
+
slotNumber: SlotNumber,
|
|
530
638
|
timestamp: bigint,
|
|
531
|
-
signalType:
|
|
639
|
+
signalType: GovernanceSignalAction,
|
|
532
640
|
payload: EthAddress,
|
|
533
641
|
base: IEmpireBase,
|
|
534
642
|
signerAddress: EthAddress,
|
|
535
643
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
536
644
|
): Promise<boolean> {
|
|
537
|
-
if (this.
|
|
645
|
+
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
646
|
+
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
538
647
|
return false;
|
|
539
648
|
}
|
|
540
649
|
if (payload.equals(EthAddress.ZERO)) {
|
|
@@ -547,14 +656,22 @@ export class SequencerPublisher {
|
|
|
547
656
|
const round = await base.computeRound(slotNumber);
|
|
548
657
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
549
658
|
|
|
659
|
+
if (roundInfo.quorumReached) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
|
|
550
663
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
551
664
|
return false;
|
|
552
665
|
}
|
|
553
666
|
|
|
554
|
-
|
|
555
|
-
|
|
667
|
+
if (await this.isPayloadEmpty(payload)) {
|
|
668
|
+
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
556
671
|
|
|
557
|
-
const
|
|
672
|
+
const cachedLastVote = this.lastActions[signalType];
|
|
673
|
+
this.lastActions[signalType] = slotNumber;
|
|
674
|
+
const action = signalType;
|
|
558
675
|
|
|
559
676
|
const request = await base.createSignalRequestWithSignature(
|
|
560
677
|
payload.toString(),
|
|
@@ -594,14 +711,14 @@ export class SequencerPublisher {
|
|
|
594
711
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
595
712
|
if (!success) {
|
|
596
713
|
this.log.error(
|
|
597
|
-
`Signaling in
|
|
714
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
598
715
|
logData,
|
|
599
716
|
);
|
|
600
|
-
this.
|
|
717
|
+
this.lastActions[signalType] = cachedLastVote;
|
|
601
718
|
return false;
|
|
602
719
|
} else {
|
|
603
720
|
this.log.info(
|
|
604
|
-
`Signaling in
|
|
721
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
605
722
|
logData,
|
|
606
723
|
);
|
|
607
724
|
return true;
|
|
@@ -611,6 +728,17 @@ export class SequencerPublisher {
|
|
|
611
728
|
return true;
|
|
612
729
|
}
|
|
613
730
|
|
|
731
|
+
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
732
|
+
const key = payload.toString();
|
|
733
|
+
const cached = this.isPayloadEmptyCache.get(key);
|
|
734
|
+
if (cached) {
|
|
735
|
+
return cached;
|
|
736
|
+
}
|
|
737
|
+
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
738
|
+
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
739
|
+
return isEmpty;
|
|
740
|
+
}
|
|
741
|
+
|
|
614
742
|
/**
|
|
615
743
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
616
744
|
* @param slotNumber - The slot number to cast a signal for.
|
|
@@ -619,7 +747,7 @@ export class SequencerPublisher {
|
|
|
619
747
|
*/
|
|
620
748
|
public enqueueGovernanceCastSignal(
|
|
621
749
|
governancePayload: EthAddress,
|
|
622
|
-
slotNumber:
|
|
750
|
+
slotNumber: SlotNumber,
|
|
623
751
|
timestamp: bigint,
|
|
624
752
|
signerAddress: EthAddress,
|
|
625
753
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
@@ -627,7 +755,7 @@ export class SequencerPublisher {
|
|
|
627
755
|
return this.enqueueCastSignalHelper(
|
|
628
756
|
slotNumber,
|
|
629
757
|
timestamp,
|
|
630
|
-
|
|
758
|
+
'governance-signal',
|
|
631
759
|
governancePayload,
|
|
632
760
|
this.govProposerContract,
|
|
633
761
|
signerAddress,
|
|
@@ -638,7 +766,7 @@ export class SequencerPublisher {
|
|
|
638
766
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
639
767
|
public async enqueueSlashingActions(
|
|
640
768
|
actions: ProposerSlashAction[],
|
|
641
|
-
slotNumber:
|
|
769
|
+
slotNumber: SlotNumber,
|
|
642
770
|
timestamp: bigint,
|
|
643
771
|
signerAddress: EthAddress,
|
|
644
772
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
@@ -661,7 +789,7 @@ export class SequencerPublisher {
|
|
|
661
789
|
await this.enqueueCastSignalHelper(
|
|
662
790
|
slotNumber,
|
|
663
791
|
timestamp,
|
|
664
|
-
|
|
792
|
+
'empire-slashing-signal',
|
|
665
793
|
action.payload,
|
|
666
794
|
this.slashingProposerContract,
|
|
667
795
|
signerAddress,
|
|
@@ -758,32 +886,24 @@ export class SequencerPublisher {
|
|
|
758
886
|
return true;
|
|
759
887
|
}
|
|
760
888
|
|
|
761
|
-
/**
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
attestations?: CommitteeAttestation[],
|
|
770
|
-
txHashes?: TxHash[],
|
|
771
|
-
opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
|
|
772
|
-
): Promise<boolean> {
|
|
773
|
-
const proposedBlockHeader = block.header.toPropose();
|
|
889
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */
|
|
890
|
+
public async enqueueProposeCheckpoint(
|
|
891
|
+
checkpoint: Checkpoint,
|
|
892
|
+
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
893
|
+
attestationsAndSignersSignature: Signature,
|
|
894
|
+
opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
|
|
895
|
+
): Promise<void> {
|
|
896
|
+
const checkpointHeader = checkpoint.header;
|
|
774
897
|
|
|
775
|
-
const
|
|
776
|
-
const
|
|
898
|
+
const blobFields = checkpoint.toBlobFields();
|
|
899
|
+
const blobs = getBlobsPerL1Block(blobFields);
|
|
777
900
|
|
|
778
|
-
const blobs = await Blob.getBlobsPerBlock(block.body.toBlobFields());
|
|
779
901
|
const proposeTxArgs = {
|
|
780
|
-
header:
|
|
781
|
-
archive:
|
|
782
|
-
stateReference: block.header.state,
|
|
783
|
-
body: block.body.toBuffer(),
|
|
902
|
+
header: checkpointHeader,
|
|
903
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
784
904
|
blobs,
|
|
785
|
-
|
|
786
|
-
|
|
905
|
+
attestationsAndSigners,
|
|
906
|
+
attestationsAndSignersSignature,
|
|
787
907
|
};
|
|
788
908
|
|
|
789
909
|
let ts: bigint;
|
|
@@ -793,21 +913,24 @@ export class SequencerPublisher {
|
|
|
793
913
|
// This means that we can avoid the simulation issues in later checks.
|
|
794
914
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
795
915
|
// make time consistency checks break.
|
|
796
|
-
const attestationData = { digest: digest.toBuffer(), attestations: attestations ?? [] };
|
|
797
916
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
798
|
-
ts = await this.
|
|
917
|
+
ts = await this.validateCheckpointForSubmission(
|
|
918
|
+
checkpoint,
|
|
919
|
+
attestationsAndSigners,
|
|
920
|
+
attestationsAndSignersSignature,
|
|
921
|
+
opts,
|
|
922
|
+
);
|
|
799
923
|
} catch (err: any) {
|
|
800
|
-
this.log.error(`
|
|
801
|
-
...
|
|
802
|
-
slotNumber:
|
|
924
|
+
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
925
|
+
...checkpoint.getStats(),
|
|
926
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
803
927
|
forcePendingBlockNumber: opts.forcePendingBlockNumber,
|
|
804
928
|
});
|
|
805
929
|
throw err;
|
|
806
930
|
}
|
|
807
931
|
|
|
808
|
-
this.log.verbose(`Enqueuing
|
|
809
|
-
await this.addProposeTx(
|
|
810
|
-
return true;
|
|
932
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
933
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
811
934
|
}
|
|
812
935
|
|
|
813
936
|
public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
|
|
@@ -818,19 +941,20 @@ export class SequencerPublisher {
|
|
|
818
941
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
819
942
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
820
943
|
|
|
821
|
-
const
|
|
944
|
+
const { gasUsed, blockNumber } = request;
|
|
945
|
+
const logData = { gasUsed, blockNumber, gasLimit, opts };
|
|
822
946
|
this.log.verbose(`Enqueuing invalidate block request`, logData);
|
|
823
947
|
this.addRequest({
|
|
824
948
|
action: `invalidate-by-${request.reason}`,
|
|
825
949
|
request: request.request,
|
|
826
950
|
gasConfig: { gasLimit, txTimeoutAt: opts.txTimeoutAt },
|
|
827
|
-
lastValidL2Slot: this.getCurrentL2Slot() +
|
|
951
|
+
lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
|
|
828
952
|
checkSuccess: (_req, result) => {
|
|
829
953
|
const success =
|
|
830
954
|
result &&
|
|
831
955
|
result.receipt &&
|
|
832
956
|
result.receipt.status === 'success' &&
|
|
833
|
-
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
957
|
+
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
834
958
|
if (!success) {
|
|
835
959
|
this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
|
|
836
960
|
} else {
|
|
@@ -842,16 +966,24 @@ export class SequencerPublisher {
|
|
|
842
966
|
}
|
|
843
967
|
|
|
844
968
|
private async simulateAndEnqueueRequest(
|
|
845
|
-
action:
|
|
969
|
+
action: Action,
|
|
846
970
|
request: L1TxRequest,
|
|
847
971
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
848
|
-
slotNumber:
|
|
972
|
+
slotNumber: SlotNumber,
|
|
849
973
|
timestamp: bigint,
|
|
850
974
|
) {
|
|
851
975
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
852
|
-
|
|
976
|
+
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
977
|
+
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const cachedLastActionSlot = this.lastActions[action];
|
|
982
|
+
this.lastActions[action] = slotNumber;
|
|
853
983
|
|
|
854
|
-
this.log.debug(`Simulating ${action}`, logData);
|
|
984
|
+
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
985
|
+
|
|
986
|
+
let gasUsed: bigint;
|
|
855
987
|
try {
|
|
856
988
|
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
857
989
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
@@ -875,6 +1007,7 @@ export class SequencerPublisher {
|
|
|
875
1007
|
const success = result && result.receipt && result.receipt.status === 'success' && checkSuccess(result.receipt);
|
|
876
1008
|
if (!success) {
|
|
877
1009
|
this.log.warn(`Action ${action} at ${slotNumber} failed`, { ...result, ...logData });
|
|
1010
|
+
this.lastActions[action] = cachedLastActionSlot;
|
|
878
1011
|
} else {
|
|
879
1012
|
this.log.info(`Action ${action} at ${slotNumber} succeeded`, { ...result, ...logData });
|
|
880
1013
|
}
|
|
@@ -904,54 +1037,58 @@ export class SequencerPublisher {
|
|
|
904
1037
|
private async prepareProposeTx(
|
|
905
1038
|
encodedData: L1ProcessArgs,
|
|
906
1039
|
timestamp: bigint,
|
|
907
|
-
options: { forcePendingBlockNumber?:
|
|
1040
|
+
options: { forcePendingBlockNumber?: BlockNumber },
|
|
908
1041
|
) {
|
|
909
1042
|
const kzg = Blob.getViemKzgInstance();
|
|
910
|
-
const blobInput =
|
|
1043
|
+
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
911
1044
|
this.log.debug('Validating blob input', { blobInput });
|
|
912
|
-
const blobEvaluationGas = await this.l1TxUtils
|
|
913
|
-
.estimateGas(
|
|
914
|
-
this.getSenderAddress().toString(),
|
|
915
|
-
{
|
|
916
|
-
to: this.rollupContract.address,
|
|
917
|
-
data: encodeFunctionData({
|
|
918
|
-
abi: RollupAbi,
|
|
919
|
-
functionName: 'validateBlobs',
|
|
920
|
-
args: [blobInput],
|
|
921
|
-
}),
|
|
922
|
-
},
|
|
923
|
-
{},
|
|
924
|
-
{
|
|
925
|
-
blobs: encodedData.blobs.map(b => b.data),
|
|
926
|
-
kzg,
|
|
927
|
-
},
|
|
928
|
-
)
|
|
929
|
-
.catch(err => {
|
|
930
|
-
const { message, metaMessages } = formatViemError(err);
|
|
931
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
932
|
-
throw new Error('Failed to validate blobs');
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
const attestations = encodedData.attestations ? encodedData.attestations.map(attest => attest.toViem()) : [];
|
|
936
|
-
const txHashes = encodedData.txHashes ? encodedData.txHashes.map(txHash => txHash.toString()) : [];
|
|
937
1045
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1046
|
+
// Get blob evaluation gas
|
|
1047
|
+
let blobEvaluationGas: bigint;
|
|
1048
|
+
if (this.config.fishermanMode) {
|
|
1049
|
+
// In fisherman mode, we can't estimate blob gas because estimateGas doesn't support state overrides
|
|
1050
|
+
// Use a fixed estimate.
|
|
1051
|
+
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
1052
|
+
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
1053
|
+
} else {
|
|
1054
|
+
// Normal mode - use estimateGas with blob inputs
|
|
1055
|
+
blobEvaluationGas = await this.l1TxUtils
|
|
1056
|
+
.estimateGas(
|
|
1057
|
+
this.getSenderAddress().toString(),
|
|
1058
|
+
{
|
|
1059
|
+
to: this.rollupContract.address,
|
|
1060
|
+
data: encodeFunctionData({
|
|
1061
|
+
abi: RollupAbi,
|
|
1062
|
+
functionName: 'validateBlobs',
|
|
1063
|
+
args: [blobInput],
|
|
1064
|
+
}),
|
|
1065
|
+
},
|
|
1066
|
+
{},
|
|
1067
|
+
{
|
|
1068
|
+
blobs: encodedData.blobs.map(b => b.data),
|
|
1069
|
+
kzg,
|
|
1070
|
+
},
|
|
1071
|
+
)
|
|
1072
|
+
.catch(err => {
|
|
1073
|
+
const { message, metaMessages } = formatViemError(err);
|
|
1074
|
+
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1075
|
+
throw new Error('Failed to validate blobs');
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
const signers = encodedData.attestationsAndSigners.getSigners().map(signer => signer.toString());
|
|
941
1079
|
|
|
942
1080
|
const args = [
|
|
943
1081
|
{
|
|
944
1082
|
header: encodedData.header.toViem(),
|
|
945
1083
|
archive: toHex(encodedData.archive),
|
|
946
|
-
stateReference: encodedData.stateReference.toViem(),
|
|
947
1084
|
oracleInput: {
|
|
948
1085
|
// We are currently not modifying these. See #9963
|
|
949
1086
|
feeAssetPriceModifier: 0n,
|
|
950
1087
|
},
|
|
951
|
-
txHashes,
|
|
952
1088
|
},
|
|
953
|
-
|
|
954
|
-
signers
|
|
1089
|
+
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
1090
|
+
signers,
|
|
1091
|
+
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
955
1092
|
blobInput,
|
|
956
1093
|
] as const;
|
|
957
1094
|
|
|
@@ -971,18 +1108,17 @@ export class SequencerPublisher {
|
|
|
971
1108
|
{
|
|
972
1109
|
readonly header: ViemHeader;
|
|
973
1110
|
readonly archive: `0x${string}`;
|
|
974
|
-
readonly stateReference: ViemStateReference;
|
|
975
|
-
readonly txHashes: `0x${string}`[];
|
|
976
1111
|
readonly oracleInput: {
|
|
977
1112
|
readonly feeAssetPriceModifier: 0n;
|
|
978
1113
|
};
|
|
979
1114
|
},
|
|
980
1115
|
ViemCommitteeAttestations,
|
|
981
|
-
`0x${string}`[],
|
|
1116
|
+
`0x${string}`[], // Signers
|
|
1117
|
+
ViemSignature,
|
|
982
1118
|
`0x${string}`,
|
|
983
1119
|
],
|
|
984
1120
|
timestamp: bigint,
|
|
985
|
-
options: { forcePendingBlockNumber?:
|
|
1121
|
+
options: { forcePendingBlockNumber?: BlockNumber },
|
|
986
1122
|
) {
|
|
987
1123
|
const rollupData = encodeFunctionData({
|
|
988
1124
|
abi: RollupAbi,
|
|
@@ -990,19 +1126,42 @@ export class SequencerPublisher {
|
|
|
990
1126
|
args,
|
|
991
1127
|
});
|
|
992
1128
|
|
|
993
|
-
// override the pending
|
|
994
|
-
const
|
|
1129
|
+
// override the pending checkpoint number if requested
|
|
1130
|
+
const optsForcePendingCheckpointNumber =
|
|
995
1131
|
options.forcePendingBlockNumber !== undefined
|
|
996
|
-
?
|
|
1132
|
+
? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
|
|
1133
|
+
: undefined;
|
|
1134
|
+
const forcePendingCheckpointNumberStateDiff = (
|
|
1135
|
+
optsForcePendingCheckpointNumber !== undefined
|
|
1136
|
+
? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber)
|
|
997
1137
|
: []
|
|
998
1138
|
).flatMap(override => override.stateDiff ?? []);
|
|
999
1139
|
|
|
1140
|
+
const stateOverrides: StateOverride = [
|
|
1141
|
+
{
|
|
1142
|
+
address: this.rollupContract.address,
|
|
1143
|
+
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1144
|
+
stateDiff: [
|
|
1145
|
+
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1146
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1147
|
+
],
|
|
1148
|
+
},
|
|
1149
|
+
];
|
|
1150
|
+
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1151
|
+
if (this.proposerAddressForSimulation) {
|
|
1152
|
+
stateOverrides.push({
|
|
1153
|
+
address: this.proposerAddressForSimulation.toString(),
|
|
1154
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1000
1158
|
const simulationResult = await this.l1TxUtils
|
|
1001
1159
|
.simulate(
|
|
1002
1160
|
{
|
|
1003
1161
|
to: this.rollupContract.address,
|
|
1004
1162
|
data: rollupData,
|
|
1005
1163
|
gas: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
1164
|
+
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1006
1165
|
},
|
|
1007
1166
|
{
|
|
1008
1167
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
@@ -1010,16 +1169,7 @@ export class SequencerPublisher {
|
|
|
1010
1169
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1011
1170
|
gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
|
|
1012
1171
|
},
|
|
1013
|
-
|
|
1014
|
-
{
|
|
1015
|
-
address: this.rollupContract.address,
|
|
1016
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1017
|
-
stateDiff: [
|
|
1018
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1019
|
-
...forcePendingBlockNumberStateDiff,
|
|
1020
|
-
],
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1172
|
+
stateOverrides,
|
|
1023
1173
|
RollupAbi,
|
|
1024
1174
|
{
|
|
1025
1175
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
@@ -1027,7 +1177,17 @@ export class SequencerPublisher {
|
|
|
1027
1177
|
},
|
|
1028
1178
|
)
|
|
1029
1179
|
.catch(err => {
|
|
1030
|
-
|
|
1180
|
+
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1181
|
+
const viemError = formatViemError(err);
|
|
1182
|
+
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1183
|
+
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1184
|
+
// Return a minimal simulation result with the fallback gas estimate
|
|
1185
|
+
return {
|
|
1186
|
+
gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
1187
|
+
logs: [],
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1031
1191
|
throw err;
|
|
1032
1192
|
});
|
|
1033
1193
|
|
|
@@ -1035,11 +1195,12 @@ export class SequencerPublisher {
|
|
|
1035
1195
|
}
|
|
1036
1196
|
|
|
1037
1197
|
private async addProposeTx(
|
|
1038
|
-
|
|
1198
|
+
checkpoint: Checkpoint,
|
|
1039
1199
|
encodedData: L1ProcessArgs,
|
|
1040
|
-
opts: { txTimeoutAt?: Date; forcePendingBlockNumber?:
|
|
1200
|
+
opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
|
|
1041
1201
|
timestamp: bigint,
|
|
1042
1202
|
): Promise<void> {
|
|
1203
|
+
const slot = checkpoint.header.slotNumber;
|
|
1043
1204
|
const timer = new Timer();
|
|
1044
1205
|
const kzg = Blob.getViemKzgInstance();
|
|
1045
1206
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
@@ -1054,11 +1215,13 @@ export class SequencerPublisher {
|
|
|
1054
1215
|
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1055
1216
|
);
|
|
1056
1217
|
|
|
1057
|
-
// Send the blobs to the blob
|
|
1058
|
-
// tx fails but it does get mined. We make sure that the blobs are sent to the blob
|
|
1059
|
-
void
|
|
1060
|
-
this.
|
|
1061
|
-
|
|
1218
|
+
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1219
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
1220
|
+
void Promise.resolve().then(() =>
|
|
1221
|
+
this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
|
|
1222
|
+
this.log.error('Failed to send blobs to blob client');
|
|
1223
|
+
}),
|
|
1224
|
+
);
|
|
1062
1225
|
|
|
1063
1226
|
return this.addRequest({
|
|
1064
1227
|
action: 'propose',
|
|
@@ -1066,7 +1229,7 @@ export class SequencerPublisher {
|
|
|
1066
1229
|
to: this.rollupContract.address,
|
|
1067
1230
|
data: rollupData,
|
|
1068
1231
|
},
|
|
1069
|
-
lastValidL2Slot:
|
|
1232
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1070
1233
|
gasConfig: { ...opts, gasLimit },
|
|
1071
1234
|
blobConfig: {
|
|
1072
1235
|
blobs: encodedData.blobs.map(b => b.data),
|
|
@@ -1080,34 +1243,41 @@ export class SequencerPublisher {
|
|
|
1080
1243
|
const success =
|
|
1081
1244
|
receipt &&
|
|
1082
1245
|
receipt.status === 'success' &&
|
|
1083
|
-
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
1246
|
+
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
1247
|
+
|
|
1084
1248
|
if (success) {
|
|
1085
1249
|
const endBlock = receipt.blockNumber;
|
|
1086
1250
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
1087
|
-
const
|
|
1251
|
+
const { calldataGas, calldataSize, sender } = stats!;
|
|
1252
|
+
const publishStats: L1PublishCheckpointStats = {
|
|
1088
1253
|
gasPrice: receipt.effectiveGasPrice,
|
|
1089
1254
|
gasUsed: receipt.gasUsed,
|
|
1090
1255
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
1091
1256
|
blobDataGas: receipt.blobGasPrice ?? 0n,
|
|
1092
1257
|
transactionHash: receipt.transactionHash,
|
|
1093
|
-
|
|
1094
|
-
|
|
1258
|
+
calldataGas,
|
|
1259
|
+
calldataSize,
|
|
1260
|
+
sender,
|
|
1261
|
+
...checkpoint.getStats(),
|
|
1095
1262
|
eventName: 'rollup-published-to-l1',
|
|
1096
1263
|
blobCount: encodedData.blobs.length,
|
|
1097
1264
|
inclusionBlocks,
|
|
1098
1265
|
};
|
|
1099
|
-
this.log.info(`Published
|
|
1266
|
+
this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
|
|
1267
|
+
...stats,
|
|
1268
|
+
...checkpoint.getStats(),
|
|
1269
|
+
...pick(receipt, 'transactionHash', 'blockHash'),
|
|
1270
|
+
});
|
|
1100
1271
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
1101
1272
|
|
|
1102
1273
|
return true;
|
|
1103
1274
|
} else {
|
|
1104
1275
|
this.metrics.recordFailedTx('process');
|
|
1105
|
-
this.log.error(
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
});
|
|
1276
|
+
this.log.error(
|
|
1277
|
+
`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
|
|
1278
|
+
undefined,
|
|
1279
|
+
{ ...checkpoint.getStats(), ...receipt },
|
|
1280
|
+
);
|
|
1111
1281
|
return false;
|
|
1112
1282
|
}
|
|
1113
1283
|
},
|