@aztec/sequencer-client 0.0.1-commit.f5d02921e → 0.0.1-commit.f650c0a5c
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/sequencer-client.d.ts +1 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +0 -4
- package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +0 -1
- package/dest/publisher/sequencer-publisher.d.ts +8 -9
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +41 -54
- package/dest/sequencer/checkpoint_proposal_job.d.ts +4 -2
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +32 -36
- package/dest/sequencer/sequencer.d.ts +1 -1
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +16 -12
- package/dest/sequencer/timetable.d.ts +4 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +15 -5
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +0 -7
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +62 -74
- package/src/sequencer/README.md +81 -12
- package/src/sequencer/checkpoint_proposal_job.ts +37 -43
- package/src/sequencer/sequencer.ts +22 -15
- package/src/sequencer/timetable.ts +19 -8
|
@@ -3,7 +3,6 @@ import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/
|
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
|
-
type EmpireSlashingProposerContract,
|
|
7
6
|
FeeAssetPriceOracle,
|
|
8
7
|
type FeeHeader,
|
|
9
8
|
type GovernanceProposerContract,
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
MULTI_CALL_3_ADDRESS,
|
|
12
11
|
Multicall3,
|
|
13
12
|
RollupContract,
|
|
14
|
-
type
|
|
13
|
+
type SlashingProposerContract,
|
|
15
14
|
type ViemCommitteeAttestations,
|
|
16
15
|
type ViemHeader,
|
|
17
16
|
} from '@aztec/ethereum/contracts';
|
|
@@ -45,7 +44,6 @@ import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slas
|
|
|
45
44
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
46
45
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
47
46
|
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
48
|
-
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
49
47
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
50
48
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
51
49
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
@@ -100,16 +98,13 @@ export const Actions = [
|
|
|
100
98
|
'invalidate-by-insufficient-attestations',
|
|
101
99
|
'propose',
|
|
102
100
|
'governance-signal',
|
|
103
|
-
'empire-slashing-signal',
|
|
104
|
-
'create-empire-payload',
|
|
105
|
-
'execute-empire-payload',
|
|
106
101
|
'vote-offenses',
|
|
107
102
|
'execute-slash',
|
|
108
103
|
] as const;
|
|
109
104
|
|
|
110
105
|
export type Action = (typeof Actions)[number];
|
|
111
106
|
|
|
112
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
107
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
113
108
|
|
|
114
109
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
115
110
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -130,6 +125,8 @@ interface RequestWithExpiry {
|
|
|
130
125
|
lastValidL2Slot: SlotNumber;
|
|
131
126
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
132
127
|
blobConfig?: L1BlobInputs;
|
|
128
|
+
/** Optional pre-send validation. If it rejects, the request is discarded. */
|
|
129
|
+
preCheck?: () => Promise<void>;
|
|
133
130
|
checkSuccess: (
|
|
134
131
|
request: L1TxRequest,
|
|
135
132
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -183,8 +180,7 @@ export class SequencerPublisher {
|
|
|
183
180
|
public l1TxUtils: L1TxUtils;
|
|
184
181
|
public rollupContract: RollupContract;
|
|
185
182
|
public govProposerContract: GovernanceProposerContract;
|
|
186
|
-
public slashingProposerContract:
|
|
187
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
183
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
188
184
|
|
|
189
185
|
public readonly tracer: Tracer;
|
|
190
186
|
|
|
@@ -198,9 +194,8 @@ export class SequencerPublisher {
|
|
|
198
194
|
blobClient: BlobClientInterface;
|
|
199
195
|
l1TxUtils: L1TxUtils;
|
|
200
196
|
rollupContract: RollupContract;
|
|
201
|
-
slashingProposerContract:
|
|
197
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
202
198
|
governanceProposerContract: GovernanceProposerContract;
|
|
203
|
-
slashFactoryContract: SlashFactoryContract;
|
|
204
199
|
epochCache: EpochCache;
|
|
205
200
|
dateProvider: DateProvider;
|
|
206
201
|
metrics: SequencerPublisherMetrics;
|
|
@@ -235,8 +230,6 @@ export class SequencerPublisher {
|
|
|
235
230
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
236
231
|
this.slashingProposerContract = newSlashingProposer;
|
|
237
232
|
});
|
|
238
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
239
|
-
|
|
240
233
|
// Initialize L1 fee analyzer for fisherman mode
|
|
241
234
|
if (config.fishermanMode) {
|
|
242
235
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
@@ -569,6 +562,28 @@ export class SequencerPublisher {
|
|
|
569
562
|
if (this.interrupted) {
|
|
570
563
|
return undefined;
|
|
571
564
|
}
|
|
565
|
+
|
|
566
|
+
// Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
|
|
567
|
+
const validRequests: RequestWithExpiry[] = [];
|
|
568
|
+
for (const request of this.requests) {
|
|
569
|
+
if (!request.preCheck) {
|
|
570
|
+
validRequests.push(request);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
await request.preCheck();
|
|
576
|
+
validRequests.push(request);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
this.requests = validRequests;
|
|
583
|
+
if (this.requests.length === 0) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
572
587
|
return this.sendRequests();
|
|
573
588
|
}
|
|
574
589
|
|
|
@@ -1078,55 +1093,6 @@ export class SequencerPublisher {
|
|
|
1078
1093
|
|
|
1079
1094
|
for (const action of actions) {
|
|
1080
1095
|
switch (action.type) {
|
|
1081
|
-
case 'vote-empire-payload': {
|
|
1082
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1083
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
1084
|
-
break;
|
|
1085
|
-
}
|
|
1086
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
1087
|
-
signerAddress,
|
|
1088
|
-
});
|
|
1089
|
-
await this.enqueueCastSignalHelper(
|
|
1090
|
-
slotNumber,
|
|
1091
|
-
'empire-slashing-signal',
|
|
1092
|
-
action.payload,
|
|
1093
|
-
this.slashingProposerContract,
|
|
1094
|
-
signerAddress,
|
|
1095
|
-
signer,
|
|
1096
|
-
);
|
|
1097
|
-
break;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
case 'create-empire-payload': {
|
|
1101
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
1102
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
1103
|
-
await this.simulateAndEnqueueRequest(
|
|
1104
|
-
'create-empire-payload',
|
|
1105
|
-
request,
|
|
1106
|
-
(receipt: TransactionReceipt) =>
|
|
1107
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
1108
|
-
slotNumber,
|
|
1109
|
-
);
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
case 'execute-empire-payload': {
|
|
1114
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
1115
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1116
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
1117
|
-
return false;
|
|
1118
|
-
}
|
|
1119
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
1120
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
1121
|
-
await this.simulateAndEnqueueRequest(
|
|
1122
|
-
'execute-empire-payload',
|
|
1123
|
-
request,
|
|
1124
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
1125
|
-
slotNumber,
|
|
1126
|
-
);
|
|
1127
|
-
break;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
1096
|
case 'vote-offenses': {
|
|
1131
1097
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
1132
1098
|
slotNumber,
|
|
@@ -1134,17 +1100,16 @@ export class SequencerPublisher {
|
|
|
1134
1100
|
votesCount: action.votes.length,
|
|
1135
1101
|
signerAddress,
|
|
1136
1102
|
});
|
|
1137
|
-
if (this.slashingProposerContract
|
|
1138
|
-
this.log.error('
|
|
1103
|
+
if (!this.slashingProposerContract) {
|
|
1104
|
+
this.log.error('No slashing proposer contract available');
|
|
1139
1105
|
return false;
|
|
1140
1106
|
}
|
|
1141
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
1142
1107
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1143
|
-
const request = await
|
|
1108
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1144
1109
|
await this.simulateAndEnqueueRequest(
|
|
1145
1110
|
'vote-offenses',
|
|
1146
1111
|
request,
|
|
1147
|
-
(receipt: TransactionReceipt) => !!
|
|
1112
|
+
(receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
|
|
1148
1113
|
slotNumber,
|
|
1149
1114
|
);
|
|
1150
1115
|
break;
|
|
@@ -1156,16 +1121,19 @@ export class SequencerPublisher {
|
|
|
1156
1121
|
round: action.round,
|
|
1157
1122
|
signerAddress,
|
|
1158
1123
|
});
|
|
1159
|
-
if (this.slashingProposerContract
|
|
1160
|
-
this.log.error('
|
|
1124
|
+
if (!this.slashingProposerContract) {
|
|
1125
|
+
this.log.error('No slashing proposer contract available');
|
|
1161
1126
|
return false;
|
|
1162
1127
|
}
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1128
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1129
|
+
action.round,
|
|
1130
|
+
action.committees,
|
|
1131
|
+
);
|
|
1165
1132
|
await this.simulateAndEnqueueRequest(
|
|
1166
1133
|
'execute-slash',
|
|
1167
|
-
|
|
1168
|
-
(receipt: TransactionReceipt) =>
|
|
1134
|
+
executeRequest,
|
|
1135
|
+
(receipt: TransactionReceipt) =>
|
|
1136
|
+
!!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
|
|
1169
1137
|
slotNumber,
|
|
1170
1138
|
);
|
|
1171
1139
|
break;
|
|
@@ -1227,8 +1195,26 @@ export class SequencerPublisher {
|
|
|
1227
1195
|
throw err;
|
|
1228
1196
|
}
|
|
1229
1197
|
|
|
1198
|
+
// Build a pre-check callback that re-validates the checkpoint before L1 submission.
|
|
1199
|
+
// During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
|
|
1200
|
+
let preCheck = undefined;
|
|
1201
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
1202
|
+
preCheck = async () => {
|
|
1203
|
+
this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
|
|
1204
|
+
await this.validateCheckpointForSubmission(
|
|
1205
|
+
checkpoint,
|
|
1206
|
+
attestationsAndSigners,
|
|
1207
|
+
attestationsAndSignersSignature,
|
|
1208
|
+
{
|
|
1209
|
+
// Forcing pending checkpoint number is included its required if an invalidation request is included
|
|
1210
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
1211
|
+
},
|
|
1212
|
+
);
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1230
1216
|
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
1231
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts);
|
|
1217
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, preCheck);
|
|
1232
1218
|
}
|
|
1233
1219
|
|
|
1234
1220
|
public enqueueInvalidateCheckpoint(
|
|
@@ -1564,6 +1550,7 @@ export class SequencerPublisher {
|
|
|
1564
1550
|
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
1565
1551
|
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
1566
1552
|
} = {},
|
|
1553
|
+
preCheck?: () => Promise<void>,
|
|
1567
1554
|
): Promise<void> {
|
|
1568
1555
|
const slot = checkpoint.header.slotNumber;
|
|
1569
1556
|
const timer = new Timer();
|
|
@@ -1592,6 +1579,7 @@ export class SequencerPublisher {
|
|
|
1592
1579
|
},
|
|
1593
1580
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1594
1581
|
gasConfig: { ...opts, gasLimit },
|
|
1582
|
+
preCheck,
|
|
1595
1583
|
blobConfig: {
|
|
1596
1584
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1597
1585
|
kzg,
|
package/src/sequencer/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Sequencer Timing Model
|
|
2
2
|
|
|
3
|
-
The Aztec sequencer divides each slot into **fixed-duration sub-slots**. Each sub-slot has a pre-defined start and end time based on an initialization offset (how much time we expect syncing the previous slot will take),
|
|
3
|
+
The Aztec sequencer divides each slot into **fixed-duration sub-slots**. Each sub-slot has a pre-defined start and end time based on an initialization offset (how much time we expect syncing the previous slot will take), the configured block duration, and whether checkpoint finalization is paid for in the current slot or deferred under proposer pipelining.
|
|
4
4
|
|
|
5
|
-
**Example: 72-second slot with 8-second sub-slots**
|
|
5
|
+
**Example: 72-second slot with 8-second sub-slots (non-pipelined)**
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
0s: Slot starts
|
|
@@ -31,7 +31,7 @@ Deadlines are fixed relative to slot start, not relative to when work actually c
|
|
|
31
31
|
|
|
32
32
|
## Overview
|
|
33
33
|
|
|
34
|
-
The Aztec sequencer operates in fixed-duration **slots** (typically 72 seconds). During each slot, a designated proposer builds multiple **blocks** containing transactions over multiple **sub-slots
|
|
34
|
+
The Aztec sequencer operates in fixed-duration **slots** (typically 72 seconds). During each slot, a designated proposer builds multiple **blocks** containing transactions over multiple **sub-slots**. In the default mode, the same slot also reserves time to collect attestations for the resulting **checkpoint**, finalize it, and publish it to L1 Ethereum. When proposer pipelining is enabled, the slot budget for block building is larger because checkpoint finalization is deferred to the next target slot.
|
|
35
35
|
|
|
36
36
|
## Key Concepts
|
|
37
37
|
|
|
@@ -42,12 +42,14 @@ The Aztec sequencer operates in fixed-duration **slots** (typically 72 seconds).
|
|
|
42
42
|
- **Checkpoint**: The collection of all blocks built in a slot, attested by validators and published to L1
|
|
43
43
|
- **Sub-slot**: A fixed-duration time window within a slot (e.g., 8 seconds) during which a block should be built
|
|
44
44
|
|
|
45
|
-
In a typical configuration, a 72-second slot contains:
|
|
45
|
+
In a typical configuration without pipelining, a 72-second slot contains:
|
|
46
46
|
- 1 initialization period (2 seconds)
|
|
47
47
|
- 5 block-building sub-slots (8 seconds each = 40 seconds)
|
|
48
48
|
- 1 last validator re-execution sub-slot (8 seconds)
|
|
49
49
|
- 1 attestation and publishing period (17 seconds)
|
|
50
50
|
|
|
51
|
+
With proposer pipelining enabled, the last validator re-execution sub-slot is still reserved, but the checkpoint finalization and L1 publishing budget is no longer subtracted when deciding how many block-building sub-slots fit in the slot.
|
|
52
|
+
|
|
51
53
|
### The Fixed Sub-Slot Model
|
|
52
54
|
|
|
53
55
|
Building multiple blocks per slot uses **fixed sub-slots** with predictable deadlines:
|
|
@@ -75,14 +77,18 @@ These values are configurable but must satisfy certain constraints (explained be
|
|
|
75
77
|
|
|
76
78
|
## Calculating Sub-Slots and Blocks
|
|
77
79
|
|
|
78
|
-
Given a slot configuration, we calculate how many blocks fit using
|
|
80
|
+
Given a slot configuration, we calculate how many blocks fit using these formulas:
|
|
79
81
|
|
|
80
82
|
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
checkpointFinalizationTime = propagationTime
|
|
84
|
+
+ propagationTime
|
|
85
|
+
+ finalizationTime
|
|
86
|
+
+ l1PublishingTime
|
|
87
|
+
|
|
88
|
+
timeReservedAtEnd (normal mode) = blockDuration (last sub-slot for reexecution)
|
|
89
|
+
+ checkpointFinalizationTime
|
|
90
|
+
|
|
91
|
+
timeReservedAtEnd (pipelining) = blockDuration (last sub-slot for reexecution only)
|
|
86
92
|
|
|
87
93
|
timeAvailableForBlocks = slotDuration - initializationOffset - timeReservedAtEnd
|
|
88
94
|
|
|
@@ -101,6 +107,62 @@ This means:
|
|
|
101
107
|
- Sub-slot 6: Reserved for validator re-execution of block 5
|
|
102
108
|
- After sub-slot 6: Attestation collection, finalization, and L1 publishing
|
|
103
109
|
|
|
110
|
+
**The same slot with proposer pipelining enabled:**
|
|
111
|
+
```
|
|
112
|
+
timeReservedAtEnd = 8s
|
|
113
|
+
timeAvailableForBlocks = 72s - 2s - 8s = 62s
|
|
114
|
+
numberOfBlocks = floor(62s / 8s) = 7 blocks
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The extra two block opportunities come from not charging the current slot for checkpoint finalization and L1 publishing.
|
|
118
|
+
|
|
119
|
+
### Pipelining Mode
|
|
120
|
+
|
|
121
|
+
When proposer pipelining is enabled, the sequencer uses the current wall-clock slot to build the checkpoint for the **next target slot**.
|
|
122
|
+
|
|
123
|
+
It helps to think in terms of two different slots:
|
|
124
|
+
|
|
125
|
+
- **Wall-clock slot N-1**: The sequencer initializes checkpoint `N`, builds its blocks, and validators re-execute the last block
|
|
126
|
+
- **Target slot N**: Checkpoint `N` is proposed, attestations are gathered, and the L1 transaction is submitted
|
|
127
|
+
|
|
128
|
+
So the work is split like this:
|
|
129
|
+
|
|
130
|
+
- **During slot N-1**: Initialization, block building, and last-block re-execution
|
|
131
|
+
- **Near the end of slot N-1**: The checkpoint proposal is broadcast and validators attest to checkpoint N.
|
|
132
|
+
- **During slot N**: The proposer collects signatures, and the checkpoint is submitted to L1
|
|
133
|
+
|
|
134
|
+
In other words, pipelining does not mean "do everything for slot N earlier". It specifically moves **block production and block re-execution** earlier, while **checkpoint proposal, attestation gathering, and L1 submission** remain aligned with slot `N`.
|
|
135
|
+
|
|
136
|
+
**Example: building checkpoint 12 while wall-clock time is in slot 11**
|
|
137
|
+
```
|
|
138
|
+
Slot 11 (wall clock):
|
|
139
|
+
- Build blocks that will make up checkpoint 12
|
|
140
|
+
- Validators re-execute the last block of checkpoint 12
|
|
141
|
+
- Broadcast checkpoint 12 proposal
|
|
142
|
+
- Collect checkpoint 12 attestations
|
|
143
|
+
|
|
144
|
+
Slot 12 (target/submission slot):
|
|
145
|
+
- Collect remaining checkpoint 12 attestations
|
|
146
|
+
- Submit checkpoint 12 to L1
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For timetable purposes, this changes two things:
|
|
150
|
+
|
|
151
|
+
- `maxNumberOfBlocks` is computed by reserving only the final validator re-execution sub-slot
|
|
152
|
+
- `initializeDeadline` no longer subtracts checkpoint finalization time; it only requires enough time for initialization, execution, and validator re-execution
|
|
153
|
+
|
|
154
|
+
In code, that means:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
initializeDeadline (normal mode) =
|
|
158
|
+
slotDuration - initializationOffset - 2 * minExecutionTime - checkpointFinalizationTime
|
|
159
|
+
|
|
160
|
+
initializeDeadline (pipelining) =
|
|
161
|
+
slotDuration - initializationOffset - 2 * minExecutionTime
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The fixed sub-slot deadlines themselves do not change. Pipelining only changes how much of the slot is considered available for block building.
|
|
165
|
+
|
|
104
166
|
## The Sequencer's Work
|
|
105
167
|
|
|
106
168
|
When elected as proposer for a slot, the sequencer performs these tasks:
|
|
@@ -226,7 +288,9 @@ After the last block is built and validators have re-executed it:
|
|
|
226
288
|
|
|
227
289
|
**Time reserved:** `2*propagationTime + finalizationTime + l1PublishingTime = 2s + 2s + 1s + 12s = 17s`
|
|
228
290
|
|
|
229
|
-
|
|
291
|
+
In the non-pipelined path, this 17s comes after the last sub-slot, ensuring we have enough time to complete the checkpoint. If the sequencer receives the necessary attestations before the reserved time, the L1 tx is submitted earlier.
|
|
292
|
+
|
|
293
|
+
With proposer pipelining enabled, this finalization budget is not charged against the current slot when calculating how many blocks fit. The checkpoint is instead queued for submission at the start of the target slot, so proposal broadcast, attestation gathering, and L1 submission happen in slot `N` while block building and block re-execution already happened in slot `N-1`.
|
|
230
294
|
|
|
231
295
|
## Handling Timing Variations
|
|
232
296
|
|
|
@@ -399,7 +463,7 @@ When configuring timing parameters, ensure these constraints are satisfied:
|
|
|
399
463
|
|
|
400
464
|
### Minimum Slot Duration
|
|
401
465
|
|
|
402
|
-
For a valid configuration:
|
|
466
|
+
For a valid multi-block configuration without pipelining:
|
|
403
467
|
```
|
|
404
468
|
slotDuration >= initializationOffset
|
|
405
469
|
+ blockDuration * 2 (at least 2 blocks)
|
|
@@ -414,6 +478,11 @@ Simplified:
|
|
|
414
478
|
slotDuration >= initializationOffset + 3*blockDuration + 2*propagationTime + finalizationTime + l1PublishingTime
|
|
415
479
|
```
|
|
416
480
|
|
|
481
|
+
With proposer pipelining enabled, the same "at least 2 buildable blocks plus the final validator re-execution sub-slot" requirement becomes:
|
|
482
|
+
```
|
|
483
|
+
slotDuration >= initializationOffset + 3*blockDuration
|
|
484
|
+
```
|
|
485
|
+
|
|
417
486
|
**Example:**
|
|
418
487
|
```
|
|
419
488
|
slotDuration >= 2s + 3*8s + 2*2s + 1s + 12s = 2s + 24s + 4s + 1s + 12s = 43s
|
|
@@ -190,8 +190,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
190
190
|
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
191
191
|
: new Date(this.dateProvider.now());
|
|
192
192
|
|
|
193
|
-
// TODO(https://github.com/AztecProtocol/aztec-packages/pull/21250): should discard the pending submission if a reorg occurs underneath
|
|
194
|
-
|
|
195
193
|
// Schedule L1 submission in the background so the work loop returns immediately.
|
|
196
194
|
// The publisher will sleep until submitAfter, then send the bundled requests.
|
|
197
195
|
// The promise is stored so it can be awaited during shutdown.
|
|
@@ -344,7 +342,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
344
342
|
};
|
|
345
343
|
|
|
346
344
|
let blocksInCheckpoint: L2Block[] = [];
|
|
347
|
-
let blockPendingBroadcast:
|
|
345
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
348
346
|
const checkpointBuildTimer = new Timer();
|
|
349
347
|
|
|
350
348
|
try {
|
|
@@ -431,19 +429,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
431
429
|
};
|
|
432
430
|
}
|
|
433
431
|
|
|
434
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
435
|
-
const lastBlock = blockPendingBroadcast && {
|
|
436
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
437
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
438
|
-
txs: blockPendingBroadcast.txs,
|
|
439
|
-
};
|
|
440
|
-
|
|
441
432
|
// Create the checkpoint proposal and broadcast it
|
|
442
433
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
443
434
|
checkpoint.header,
|
|
444
435
|
checkpoint.archive.root,
|
|
445
436
|
feeAssetPriceModifier,
|
|
446
|
-
|
|
437
|
+
blockPendingBroadcast,
|
|
447
438
|
this.proposer,
|
|
448
439
|
checkpointProposalOptions,
|
|
449
440
|
);
|
|
@@ -500,14 +491,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
500
491
|
blockProposalOptions: BlockProposalOptions,
|
|
501
492
|
): Promise<{
|
|
502
493
|
blocksInCheckpoint: L2Block[];
|
|
503
|
-
blockPendingBroadcast:
|
|
494
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
504
495
|
}> {
|
|
505
496
|
const blocksInCheckpoint: L2Block[] = [];
|
|
506
497
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
507
498
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
508
499
|
|
|
509
500
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
510
|
-
let blockPendingBroadcast:
|
|
501
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
511
502
|
|
|
512
503
|
while (true) {
|
|
513
504
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
@@ -540,19 +531,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
540
531
|
txHashesAlreadyIncluded,
|
|
541
532
|
});
|
|
542
533
|
|
|
543
|
-
//
|
|
544
|
-
if (
|
|
545
|
-
// If
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
534
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
535
|
+
if ('failure' in buildResult) {
|
|
536
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
537
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
549
541
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
550
542
|
continue;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
546
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
547
|
+
if ('error' in buildResult) {
|
|
556
548
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
557
549
|
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
558
550
|
slot: this.targetSlot,
|
|
@@ -567,30 +559,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
567
559
|
blocksInCheckpoint.push(block);
|
|
568
560
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
569
561
|
|
|
570
|
-
//
|
|
571
|
-
|
|
562
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
563
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
564
|
+
|
|
565
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
566
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
567
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
568
|
+
// If this throws, we abort the entire checkpoint.
|
|
569
|
+
await this.syncProposedBlockToArchiver(block);
|
|
570
|
+
|
|
571
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
572
572
|
if (timingInfo.isLastBlock) {
|
|
573
|
-
await this.syncProposedBlockToArchiver(block);
|
|
574
573
|
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
575
574
|
slot: this.targetSlot,
|
|
576
575
|
blockNumber,
|
|
577
576
|
blocksBuilt,
|
|
578
577
|
});
|
|
579
|
-
blockPendingBroadcast =
|
|
578
|
+
blockPendingBroadcast = proposal;
|
|
580
579
|
break;
|
|
581
580
|
}
|
|
582
581
|
|
|
583
|
-
// Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
|
|
584
|
-
// in which case we'll broadcast it along with the checkpoint at the end of the loop.
|
|
585
|
-
// Note that we only send the block to the archiver if we manage to create the proposal, so if there's
|
|
586
|
-
// a HA error we don't pollute our archiver with a block that won't make it to the chain.
|
|
587
|
-
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
588
|
-
|
|
589
|
-
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
|
|
590
|
-
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
591
|
-
// If this throws, we abort the entire checkpoint.
|
|
592
|
-
await this.syncProposedBlockToArchiver(block);
|
|
593
|
-
|
|
594
582
|
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
595
583
|
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
596
584
|
|
|
@@ -650,7 +638,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
650
638
|
buildDeadline: Date | undefined;
|
|
651
639
|
txHashesAlreadyIncluded: Set<string>;
|
|
652
640
|
},
|
|
653
|
-
): Promise<
|
|
641
|
+
): Promise<
|
|
642
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
643
|
+
> {
|
|
654
644
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
655
645
|
opts;
|
|
656
646
|
|
|
@@ -669,7 +659,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
669
659
|
);
|
|
670
660
|
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
671
661
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
672
|
-
return
|
|
662
|
+
return { failure: 'insufficient-txs' };
|
|
673
663
|
}
|
|
674
664
|
|
|
675
665
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
@@ -732,7 +722,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
732
722
|
slot: this.targetSlot,
|
|
733
723
|
});
|
|
734
724
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
735
|
-
return
|
|
725
|
+
return { failure: 'insufficient-valid-txs' };
|
|
736
726
|
}
|
|
737
727
|
|
|
738
728
|
// Block creation succeeded, emit stats and metrics
|
|
@@ -1012,9 +1002,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1012
1002
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
1013
1003
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
1014
1004
|
* would never receive its own block without this explicit sync.
|
|
1005
|
+
*
|
|
1006
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1007
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1008
|
+
* whenever the real proposer's block arrives from L1.
|
|
1015
1009
|
*/
|
|
1016
1010
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
1017
|
-
if (this.config.skipPushProposedBlocksToArchiver) {
|
|
1011
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
1018
1012
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
1019
1013
|
blockNumber: block.number,
|
|
1020
1014
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -123,6 +123,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
123
123
|
p2pPropagationTime: this.config.attestationPropagationTime,
|
|
124
124
|
blockDurationMs: this.config.blockDurationMs,
|
|
125
125
|
enforce: this.config.enforceTimeTable,
|
|
126
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
126
127
|
},
|
|
127
128
|
this.metrics,
|
|
128
129
|
this.log,
|
|
@@ -299,16 +300,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
299
300
|
// Next checkpoint follows from the last synced one
|
|
300
301
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
301
302
|
|
|
302
|
-
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
303
|
-
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
304
|
-
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
305
|
-
if (checkpointNumber > confirmedCkpt + 2) {
|
|
306
|
-
this.log.warn(
|
|
307
|
-
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
308
|
-
);
|
|
309
|
-
return undefined;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
303
|
const logCtx = {
|
|
313
304
|
nowSeconds,
|
|
314
305
|
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
@@ -329,6 +320,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
329
320
|
return undefined;
|
|
330
321
|
}
|
|
331
322
|
|
|
323
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
324
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
325
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
326
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
327
|
+
this.log.verbose(
|
|
328
|
+
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
329
|
+
);
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
332
333
|
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
333
334
|
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
334
335
|
this.log.warn(
|
|
@@ -567,12 +568,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
567
568
|
.getL2Tips()
|
|
568
569
|
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
569
570
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
570
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
571
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
571
572
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
572
573
|
this.l2BlockSource.getProposedCheckpointOnly(),
|
|
573
574
|
] as const);
|
|
574
575
|
|
|
575
|
-
const [worldState, l2Tips, p2p,
|
|
576
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
576
577
|
syncedBlocks;
|
|
577
578
|
|
|
578
579
|
// Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
|
|
@@ -580,19 +581,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
580
581
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
581
582
|
const result =
|
|
582
583
|
(l2Tips.proposed.number === 0 &&
|
|
584
|
+
l2Tips.checkpointed.block.number === 0 &&
|
|
585
|
+
l2Tips.checkpointed.checkpoint.number === 0 &&
|
|
583
586
|
worldState.number === 0 &&
|
|
584
587
|
p2p.number === 0 &&
|
|
585
|
-
|
|
588
|
+
l1ToL2MessageSourceTips.proposed.number === 0 &&
|
|
589
|
+
l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
|
|
590
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
|
|
586
591
|
(worldState.hash === l2Tips.proposed.hash &&
|
|
587
592
|
p2p.hash === l2Tips.proposed.hash &&
|
|
588
|
-
|
|
593
|
+
l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
|
|
594
|
+
l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
|
|
595
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
|
|
589
596
|
|
|
590
597
|
if (!result) {
|
|
591
598
|
this.log.debug(`Sequencer sync check failed`, {
|
|
592
599
|
worldState,
|
|
593
600
|
l2BlockSource: l2Tips.proposed,
|
|
594
601
|
p2p,
|
|
595
|
-
|
|
602
|
+
l1ToL2MessageSourceTips,
|
|
596
603
|
});
|
|
597
604
|
return undefined;
|
|
598
605
|
}
|