@aztec/sequencer-client 5.0.0-rc.1 → 5.0.0-rc.2
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/config.js +1 -1
- package/dest/global_variable_builder/global_builder.d.ts +3 -16
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +2 -25
- package/dest/publisher/config.d.ts +5 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +11 -1
- package/dest/publisher/sequencer-publisher.d.ts +17 -4
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +113 -20
- package/dest/sequencer/automine/automine_sequencer.d.ts +8 -3
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_sequencer.js +16 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +2 -3
- package/dest/sequencer/sequencer.d.ts +9 -7
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +46 -41
- package/dest/test/utils.d.ts +1 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +1 -1
- package/package.json +27 -27
- package/src/config.ts +1 -1
- package/src/global_variable_builder/global_builder.ts +2 -34
- package/src/publisher/config.ts +22 -1
- package/src/publisher/sequencer-publisher.ts +108 -21
- package/src/sequencer/automine/automine_sequencer.ts +16 -4
- package/src/sequencer/checkpoint_proposal_job.ts +3 -5
- package/src/sequencer/sequencer.ts +52 -48
- package/src/test/utils.ts +1 -0
- package/dest/sequencer/chain_state_overrides.d.ts +0 -61
- package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
- package/dest/sequencer/chain_state_overrides.js +0 -98
- package/src/sequencer/chain_state_overrides.ts +0 -169
package/src/publisher/config.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { type BlobClientConfig, blobClientConfigMapping } from '@aztec/blob-client/client/config';
|
|
2
2
|
import { type L1ReaderConfig, l1ReaderConfigMappings } from '@aztec/ethereum/l1-reader';
|
|
3
3
|
import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from '@aztec/ethereum/l1-tx-utils/config';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type ConfigMappingsType,
|
|
6
|
+
SecretValue,
|
|
7
|
+
booleanConfigHelper,
|
|
8
|
+
numberConfigHelper,
|
|
9
|
+
} from '@aztec/foundation/config';
|
|
5
10
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
11
|
|
|
7
12
|
import { parseEther } from 'viem';
|
|
@@ -90,6 +95,10 @@ export type SequencerPublisherConfig = L1TxUtilsConfig &
|
|
|
90
95
|
fishermanMode?: boolean;
|
|
91
96
|
sequencerPublisherAllowInvalidStates?: boolean;
|
|
92
97
|
sequencerPublisherForwarderAddress?: EthAddress;
|
|
98
|
+
/** How long to wait for the previous L1 block before sending scheduled publisher txs anyway. */
|
|
99
|
+
sequencerPublisherPreviousL1BlockWaitTimeoutMs: number;
|
|
100
|
+
/** Poll interval while waiting for the previous L1 block before scheduled publisher txs. */
|
|
101
|
+
sequencerPublisherPreviousL1BlockWaitPollIntervalMs: number;
|
|
93
102
|
/** Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path */
|
|
94
103
|
l1TxFailedStore?: string;
|
|
95
104
|
/** Min ETH balance below which a publisher gets funded. Undefined = funding disabled. */
|
|
@@ -170,6 +179,18 @@ export const sequencerPublisherConfigMappings: ConfigMappingsType<SequencerPubli
|
|
|
170
179
|
description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
|
|
171
180
|
parseEnv: (val: string) => EthAddress.fromString(val),
|
|
172
181
|
},
|
|
182
|
+
sequencerPublisherPreviousL1BlockWaitTimeoutMs: {
|
|
183
|
+
env: `SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_TIMEOUT_MS`,
|
|
184
|
+
description:
|
|
185
|
+
'How long to wait for the previous L1 block before sending scheduled publisher txs anyway, in milliseconds.',
|
|
186
|
+
...numberConfigHelper(8_000),
|
|
187
|
+
},
|
|
188
|
+
sequencerPublisherPreviousL1BlockWaitPollIntervalMs: {
|
|
189
|
+
env: `SEQ_PUBLISHER_PREVIOUS_L1_BLOCK_WAIT_POLL_INTERVAL_MS`,
|
|
190
|
+
description:
|
|
191
|
+
'Poll interval while waiting for the previous L1 block before scheduled publisher txs, in milliseconds.',
|
|
192
|
+
...numberConfigHelper(500),
|
|
193
|
+
},
|
|
173
194
|
l1TxFailedStore: {
|
|
174
195
|
env: 'L1_TX_FAILED_STORE',
|
|
175
196
|
description: 'Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path',
|
|
@@ -117,6 +117,7 @@ type L1ProcessArgs = {
|
|
|
117
117
|
export const Actions = [
|
|
118
118
|
'invalidate-by-invalid-attestation',
|
|
119
119
|
'invalidate-by-insufficient-attestations',
|
|
120
|
+
'prune',
|
|
120
121
|
'propose',
|
|
121
122
|
'governance-signal',
|
|
122
123
|
'vote-offenses',
|
|
@@ -177,6 +178,8 @@ export class SequencerPublisher {
|
|
|
177
178
|
protected log: Logger;
|
|
178
179
|
protected ethereumSlotDuration: bigint;
|
|
179
180
|
protected aztecSlotDuration: bigint;
|
|
181
|
+
private readonly previousL1BlockWaitTimeoutMs: number;
|
|
182
|
+
private readonly previousL1BlockWaitPollIntervalMs: number;
|
|
180
183
|
|
|
181
184
|
/** Date provider for wall-clock time. */
|
|
182
185
|
private readonly dateProvider: DateProvider;
|
|
@@ -205,7 +208,13 @@ export class SequencerPublisher {
|
|
|
205
208
|
protected requests: RequestWithExpiry[] = [];
|
|
206
209
|
|
|
207
210
|
constructor(
|
|
208
|
-
private config: Pick<
|
|
211
|
+
private config: Pick<
|
|
212
|
+
SequencerPublisherConfig,
|
|
213
|
+
| 'fishermanMode'
|
|
214
|
+
| 'l1TxFailedStore'
|
|
215
|
+
| 'sequencerPublisherPreviousL1BlockWaitTimeoutMs'
|
|
216
|
+
| 'sequencerPublisherPreviousL1BlockWaitPollIntervalMs'
|
|
217
|
+
> &
|
|
209
218
|
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
210
219
|
deps: {
|
|
211
220
|
telemetry?: TelemetryClient;
|
|
@@ -225,6 +234,8 @@ export class SequencerPublisher {
|
|
|
225
234
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
226
235
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
227
236
|
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
237
|
+
this.previousL1BlockWaitTimeoutMs = config.sequencerPublisherPreviousL1BlockWaitTimeoutMs;
|
|
238
|
+
this.previousL1BlockWaitPollIntervalMs = config.sequencerPublisherPreviousL1BlockWaitPollIntervalMs;
|
|
228
239
|
this.dateProvider = deps.dateProvider;
|
|
229
240
|
this.epochCache = deps.epochCache;
|
|
230
241
|
this.lastActions = deps.lastActions;
|
|
@@ -617,34 +628,69 @@ export class SequencerPublisher {
|
|
|
617
628
|
|
|
618
629
|
/*
|
|
619
630
|
* Schedules sending all enqueued requests at (or after) the start of the given L2 slot.
|
|
620
|
-
* Sleeps until one L1 slot before the L2 slot boundary so the tx has a chance of being
|
|
621
|
-
* picked up by the first L1 block of the L2 slot.
|
|
622
|
-
* NB: there is a known correctness risk — being included in the L1 block right before the
|
|
623
|
-
* L2 slot starts would revert propose with HeaderLib__InvalidSlotNumber.
|
|
624
|
-
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
625
631
|
*/
|
|
626
632
|
public async sendRequestsAt(targetSlot: SlotNumber): Promise<SendRequestsResult | undefined> {
|
|
627
|
-
|
|
628
|
-
// Start of the target L2 slot, in ms (getTimestampForSlot returns seconds).
|
|
629
|
-
const startOfTargetSlotMs = Number(getTimestampForSlot(targetSlot, l1Constants)) * 1000;
|
|
630
|
-
// Aim to be in the mempool one L1 slot before the L2 slot starts, so we have a chance of
|
|
631
|
-
// being picked up by the first L1 block of the L2 slot.
|
|
632
|
-
const submitAfterMs = startOfTargetSlotMs - Number(this.ethereumSlotDuration) * 1000;
|
|
633
|
+
await this.waitForTargetSlot(targetSlot);
|
|
633
634
|
if (this.interrupted) {
|
|
634
635
|
return undefined;
|
|
635
636
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
637
|
+
|
|
638
|
+
return this.sendRequests(targetSlot);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Sleeps until one L1 slot before the L2 slot boundary, and then waits for that L1 block
|
|
643
|
+
* to be mined, so we don't risk being included in it. If that block never gets mined after
|
|
644
|
+
* a timeout, we assume it got skipped on L1, so we send the tx anyway.
|
|
645
|
+
*/
|
|
646
|
+
private async waitForTargetSlot(targetSlot: SlotNumber): Promise<void> {
|
|
647
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
648
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
649
|
+
const startOfTargetSlotTs = getTimestampForSlot(targetSlot, l1Constants);
|
|
650
|
+
const previousL1BlockTs = startOfTargetSlotTs - this.ethereumSlotDuration;
|
|
651
|
+
const waitDeadlineTs = previousL1BlockTs + BigInt(this.previousL1BlockWaitTimeoutMs / 1000);
|
|
652
|
+
const logCtx = { targetSlot, startOfTargetSlotTs, nowInSeconds, previousL1BlockTs, waitDeadlineTs };
|
|
653
|
+
|
|
654
|
+
// Check if we are already past time
|
|
655
|
+
if (nowInSeconds >= startOfTargetSlotTs) {
|
|
656
|
+
this.log.verbose(`Target slot ${targetSlot} already started, sending requests immediately`, logCtx);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Otherwise we wait
|
|
661
|
+
this.log.debug(`Waiting for slot ${targetSlot} before sending requests`, logCtx);
|
|
662
|
+
|
|
663
|
+
// Wait until previous L1 block timestamp first
|
|
664
|
+
const sleepMs = (Number(previousL1BlockTs) - nowInSeconds) * 1000;
|
|
665
|
+
if (sleepMs > 0 && !this.interrupted) {
|
|
666
|
+
this.log.trace(`Sleeping ${sleepMs}ms before waiting for previous L1 block`, logCtx);
|
|
642
667
|
await this.interruptibleSleep.sleep(sleepMs);
|
|
643
668
|
}
|
|
644
|
-
|
|
645
|
-
|
|
669
|
+
|
|
670
|
+
// Then loop until we see the previous L1 block, so we know that we cannot be included in it.
|
|
671
|
+
// We time out after a while, once we are sure that that block is skipped in L1.
|
|
672
|
+
while (!this.interrupted) {
|
|
673
|
+
try {
|
|
674
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
675
|
+
logCtx.nowInSeconds = nowInSeconds;
|
|
676
|
+
|
|
677
|
+
if (nowInSeconds >= waitDeadlineTs) {
|
|
678
|
+
this.log.warn(`Timed out waiting for previous L1 block before sending requests, proceeding`, logCtx);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const latestBlockTs = await this.l1TxUtils.getBlock().then(b => b.timestamp);
|
|
683
|
+
if (latestBlockTs >= previousL1BlockTs) {
|
|
684
|
+
this.log.debug(`Previous L1 block mined, proceeding to send requests`, { ...logCtx, latestBlockTs });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
this.log.trace(`Previous L1 block not mined yet, continuing to wait`, { ...logCtx, latestBlockTs });
|
|
688
|
+
} catch (err) {
|
|
689
|
+
this.log.error(`Error while waiting for previous L1 block before sending requests; retrying`, err, logCtx);
|
|
690
|
+
} finally {
|
|
691
|
+
await this.interruptibleSleep.sleep(this.previousL1BlockWaitPollIntervalMs);
|
|
692
|
+
}
|
|
646
693
|
}
|
|
647
|
-
return this.sendRequests(targetSlot);
|
|
648
694
|
}
|
|
649
695
|
|
|
650
696
|
private callbackBundledTransactions(
|
|
@@ -1015,6 +1061,47 @@ export class SequencerPublisher {
|
|
|
1015
1061
|
);
|
|
1016
1062
|
}
|
|
1017
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Enqueues a `prune()` transaction if the rollup is prunable at the given slot's L1 timestamp.
|
|
1066
|
+
* `prune()` is permissionless and idempotent — if the chain is no longer prunable by send time the
|
|
1067
|
+
* bundle simulation usually drops the entry; on a node without `eth_simulateV1` the bundle is sent
|
|
1068
|
+
* as-is and the prune reverts `Rollup__NothingToPrune` inside `aggregate3(allowFailure: true)`
|
|
1069
|
+
* (a failed action, never a whole-tx revert). Used by the failed-sync fallback so a stuck pending
|
|
1070
|
+
* chain (e.g. bad data blocking sync) can be wound back to recover.
|
|
1071
|
+
* @returns true if a prune request was enqueued, false otherwise.
|
|
1072
|
+
*/
|
|
1073
|
+
public async enqueuePruneIfPrunable(slotNumber: SlotNumber): Promise<boolean> {
|
|
1074
|
+
if (this.lastActions['prune'] === slotNumber) {
|
|
1075
|
+
this.log.debug(`Skipping duplicate prune for slot ${slotNumber}`, { slotNumber });
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
// Use the SAME timestamp the bundle simulator overrides block.timestamp with at send time
|
|
1079
|
+
// (sequencer-bundle-simulator.ts) so this upfront check and the send-time sim agree. Slot-start
|
|
1080
|
+
// and last-L1-slot both fall within the same L2 slot (and epoch, which is what `canPruneAtTime`
|
|
1081
|
+
// derives), so they agree today; matching the simulator keeps it robust if the contract ever uses
|
|
1082
|
+
// the timestamp more granularly.
|
|
1083
|
+
const ts = getLastL1SlotTimestampForL2Slot(slotNumber, this.epochCache.getL1Constants());
|
|
1084
|
+
const canPrune = await this.rollupContract.canPruneAtTime(ts).catch(err => {
|
|
1085
|
+
this.log.error(`Failed to check canPruneAtTime for slot ${slotNumber}`, err, { slotNumber });
|
|
1086
|
+
return false;
|
|
1087
|
+
});
|
|
1088
|
+
if (!canPrune) {
|
|
1089
|
+
this.log.debug(`Rollup not prunable at slot ${slotNumber}`, { slotNumber });
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
const request: L1TxRequest = {
|
|
1093
|
+
to: this.rollupContract.address,
|
|
1094
|
+
data: encodeFunctionData({ abi: RollupAbi, functionName: 'prune', args: [] }),
|
|
1095
|
+
};
|
|
1096
|
+
this.log.info(`Enqueuing rollup prune for slot ${slotNumber}`, { slotNumber });
|
|
1097
|
+
return this.enqueueRequest(
|
|
1098
|
+
'prune',
|
|
1099
|
+
request,
|
|
1100
|
+
{ address: this.rollupContract.address, abi: RollupAbi, eventName: 'PrunedPending' },
|
|
1101
|
+
slotNumber,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1018
1105
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
1019
1106
|
public async enqueueSlashingActions(
|
|
1020
1107
|
actions: ProposerSlashAction[],
|
|
@@ -274,14 +274,22 @@ export class AutomineSequencer {
|
|
|
274
274
|
/**
|
|
275
275
|
* Warps L1 timestamp to `targetTimestampSec`. Rounded up to the next aztec-slot
|
|
276
276
|
* boundary so the next build lands on a fresh slot. Atomic with respect to builds —
|
|
277
|
-
* the queue ensures no build is in flight while the warp executes.
|
|
277
|
+
* the queue ensures no build is in flight while the warp executes. A no-op when the
|
|
278
|
+
* target is already at or behind the current L1 time (see {@link runWarp}).
|
|
278
279
|
*/
|
|
279
280
|
public warpTo(targetTimestampSec: number): Promise<void> {
|
|
280
281
|
return this.queue.put(() => this.runWarp(targetTimestampSec));
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* Warps L1 timestamp forward by `deltaSec` seconds from the current L1 time, rounded up to the next
|
|
286
|
+
* aztec-slot boundary. Throws if `deltaSec` is not positive (warping "by" a non-positive amount is a
|
|
287
|
+
* caller bug — unlike {@link warpTo}, which no-ops on a past target).
|
|
288
|
+
*/
|
|
284
289
|
public warpBy(deltaSec: number): Promise<void> {
|
|
290
|
+
if (deltaSec <= 0) {
|
|
291
|
+
throw new Error(`warpL2TimeAtLeastBy: duration must be positive, got ${deltaSec} seconds.`);
|
|
292
|
+
}
|
|
285
293
|
return this.queue.put(async () => {
|
|
286
294
|
const current = await this.deps.ethCheatCodes.lastBlockTimestamp();
|
|
287
295
|
await this.runWarp(current + deltaSec);
|
|
@@ -409,7 +417,10 @@ export class AutomineSequencer {
|
|
|
409
417
|
await this.deps.ethCheatCodes.setNextBlockTimestamp(slotBoundaryTs);
|
|
410
418
|
}
|
|
411
419
|
|
|
412
|
-
const tips = await
|
|
420
|
+
const [tips, proposedCheckpoint] = await Promise.all([
|
|
421
|
+
this.deps.l2BlockSource.getL2Tips(),
|
|
422
|
+
this.deps.l2BlockSource.getProposedCheckpointData(),
|
|
423
|
+
]);
|
|
413
424
|
const syncedToBlockNumber = tips.proposed.number;
|
|
414
425
|
|
|
415
426
|
// Ensure world state has processed the archiver's tip before forking. Without this,
|
|
@@ -419,7 +430,8 @@ export class AutomineSequencer {
|
|
|
419
430
|
await this.deps.worldState.syncImmediate(BlockNumber(syncedToBlockNumber));
|
|
420
431
|
|
|
421
432
|
const nextBlockNumber = BlockNumber(syncedToBlockNumber + 1);
|
|
422
|
-
const
|
|
433
|
+
const parentCheckpointNumber = proposedCheckpoint?.checkpointNumber ?? tips.checkpointed.checkpoint.number;
|
|
434
|
+
const checkpointNumber = CheckpointNumber(parentCheckpointNumber + 1);
|
|
423
435
|
const targetEpoch = getEpochAtSlot(SlotNumber(targetSlot), this.deps.l1Constants);
|
|
424
436
|
|
|
425
437
|
this.log.verbose(`Building automine checkpoint`, {
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
type Checkpoint,
|
|
39
39
|
type ProposedCheckpointData,
|
|
40
|
+
buildCheckpointSimulationOverridesPlan,
|
|
40
41
|
getPreviousCheckpointOutHashes,
|
|
41
42
|
validateCheckpoint,
|
|
42
43
|
} from '@aztec/stdlib/checkpoint';
|
|
@@ -68,7 +69,6 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
68
69
|
|
|
69
70
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
70
71
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
71
|
-
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
72
72
|
import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
|
|
73
73
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
74
74
|
import { SequencerInterruptedError } from './errors.js';
|
|
@@ -398,9 +398,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
402
|
-
txTimeoutAt,
|
|
403
|
-
});
|
|
401
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, { txTimeoutAt });
|
|
404
402
|
}
|
|
405
403
|
|
|
406
404
|
/**
|
|
@@ -1062,7 +1060,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1062
1060
|
availableTxs,
|
|
1063
1061
|
minTxs,
|
|
1064
1062
|
});
|
|
1065
|
-
this.log.
|
|
1063
|
+
this.log.verbose(
|
|
1066
1064
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
1067
1065
|
{
|
|
1068
1066
|
reason: 'insufficient_txs',
|
|
@@ -18,7 +18,11 @@ import type {
|
|
|
18
18
|
ProposedCheckpointSink,
|
|
19
19
|
ValidateCheckpointResult,
|
|
20
20
|
} from '@aztec/stdlib/block';
|
|
21
|
-
import
|
|
21
|
+
import {
|
|
22
|
+
type Checkpoint,
|
|
23
|
+
type ProposedCheckpointData,
|
|
24
|
+
buildCheckpointSimulationOverridesPlan,
|
|
25
|
+
} from '@aztec/stdlib/checkpoint';
|
|
22
26
|
import type { ChainConfig } from '@aztec/stdlib/config';
|
|
23
27
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
24
28
|
import {
|
|
@@ -45,7 +49,6 @@ import { DefaultSequencerConfig } from '../config.js';
|
|
|
45
49
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
46
50
|
import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
|
|
47
51
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
48
|
-
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
49
52
|
import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
|
|
50
53
|
import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
|
|
51
54
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
@@ -85,8 +88,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
85
88
|
private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
|
|
86
89
|
private readonly stateLog: Logger;
|
|
87
90
|
|
|
88
|
-
/** The last slot for which we attempted to perform our
|
|
89
|
-
private
|
|
91
|
+
/** The last slot for which we attempted to perform our fallback duties (votes and/or prune) with degraded block production */
|
|
92
|
+
private lastSlotForFallbackAction: SlotNumber | undefined;
|
|
90
93
|
|
|
91
94
|
/** The (checkpoint, slot) of the last invalidation request we successfully simulated, to prevent
|
|
92
95
|
* re-simulating and re-submitting the same invalidation across the many ticks within a single slot. */
|
|
@@ -452,7 +455,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
452
455
|
// We are the proposer and the escape hatch is closed: now run the full sync check before building.
|
|
453
456
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
454
457
|
if (!syncedTo) {
|
|
455
|
-
await this.
|
|
458
|
+
await this.tryVoteAndPruneWhenCannotBuild({ slot, targetSlot });
|
|
456
459
|
return undefined;
|
|
457
460
|
}
|
|
458
461
|
|
|
@@ -472,7 +475,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
472
475
|
});
|
|
473
476
|
// Mark the slot as attempted so a deadline abort is not retried within the same slot. Vote-only actions
|
|
474
477
|
// still need to run because sync can succeed even when it is too late to start building a checkpoint.
|
|
475
|
-
await this.
|
|
478
|
+
await this.tryVoteAndPruneWhenCannotBuild({ slot, targetSlot });
|
|
476
479
|
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
477
480
|
return undefined;
|
|
478
481
|
}
|
|
@@ -798,9 +801,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
798
801
|
number: syncSummary.latestBlockNumber,
|
|
799
802
|
hash: syncSummary.latestBlockHash,
|
|
800
803
|
})),
|
|
801
|
-
this.l2BlockSource
|
|
802
|
-
.getL2Tips()
|
|
803
|
-
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
804
|
+
this.l2BlockSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
804
805
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
805
806
|
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
806
807
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
@@ -845,16 +846,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
845
846
|
// matching proposed checkpoint (e.g. it crashed before assembling it). Building on this orphan block
|
|
846
847
|
// would fork the chain off a tip no other node can follow. The archiver prunes these orphan blocks
|
|
847
848
|
// once their build slot ends; this guard is the correctness barrier during the grace window before.
|
|
849
|
+
// `getProposedCheckpointData()` returns the latest proposed checkpoint payload, which is always
|
|
850
|
+
// the leading one (a proposed entry is only stored beyond the confirmed frontier and is deleted
|
|
851
|
+
// on confirmation). It carries no tip, so there is no tip-vs-payload split read to reconcile.
|
|
848
852
|
if (
|
|
849
853
|
blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number &&
|
|
850
|
-
|
|
851
|
-
proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber)
|
|
854
|
+
proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber
|
|
852
855
|
) {
|
|
853
856
|
const logCtx = {
|
|
854
857
|
blockCheckpointNumber: blockData.checkpointNumber,
|
|
855
858
|
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
856
|
-
proposedCheckpointTipNumber:
|
|
857
|
-
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
859
|
+
proposedCheckpointTipNumber: proposedCheckpointData?.checkpointNumber,
|
|
858
860
|
blockNumber: blockData.header.getBlockNumber(),
|
|
859
861
|
blockSlot: blockData.header.getSlot(),
|
|
860
862
|
syncedL2Slot,
|
|
@@ -865,27 +867,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
865
867
|
return undefined;
|
|
866
868
|
}
|
|
867
869
|
|
|
868
|
-
const hasProposedCheckpoint =
|
|
869
|
-
|
|
870
|
-
// The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots
|
|
871
|
-
// (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver
|
|
872
|
-
// write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but
|
|
873
|
-
// no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that
|
|
874
|
-
// window — the next checkSync tick will see a coherent snapshot.
|
|
875
|
-
if (
|
|
876
|
-
hasProposedCheckpoint &&
|
|
877
|
-
(!proposedCheckpointData ||
|
|
878
|
-
proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number)
|
|
879
|
-
) {
|
|
880
|
-
this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, {
|
|
881
|
-
proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
|
|
882
|
-
checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number,
|
|
883
|
-
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
884
|
-
syncedL2Slot,
|
|
885
|
-
...args,
|
|
886
|
-
});
|
|
887
|
-
return undefined;
|
|
888
|
-
}
|
|
870
|
+
const hasProposedCheckpoint = proposedCheckpointData !== undefined;
|
|
889
871
|
|
|
890
872
|
// Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building
|
|
891
873
|
// The checkpoint number to build is derived as blockData.checkpointNumber + 1
|
|
@@ -962,16 +944,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
962
944
|
}
|
|
963
945
|
|
|
964
946
|
/**
|
|
965
|
-
* Tries to vote on slashing actions and governance when we cannot build and are past the
|
|
966
|
-
* This allows the sequencer to participate in governance/slashing votes even when it
|
|
947
|
+
* Tries to vote on slashing actions and governance and to prune when we cannot build and are past the
|
|
948
|
+
* block-building window. This allows the sequencer to participate in governance/slashing votes even when it
|
|
949
|
+
* cannot build blocks, and to prune the pending chain so it can recover from bad data that is blocking sync.
|
|
967
950
|
*/
|
|
968
|
-
@trackSpan('Sequencer.
|
|
969
|
-
protected async
|
|
951
|
+
@trackSpan('Sequencer.tryVoteAndPruneWhenCannotBuild', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
952
|
+
protected async tryVoteAndPruneWhenCannotBuild(args: { slot: SlotNumber; targetSlot: SlotNumber }): Promise<void> {
|
|
970
953
|
const { slot, targetSlot } = args;
|
|
971
954
|
|
|
972
955
|
// Prevent duplicate attempts in the same slot
|
|
973
|
-
if (this.
|
|
974
|
-
this.log.trace(`Already attempted
|
|
956
|
+
if (this.lastSlotForFallbackAction === slot) {
|
|
957
|
+
this.log.trace(`Already attempted fallback actions in slot ${slot} (skipping)`);
|
|
975
958
|
return;
|
|
976
959
|
}
|
|
977
960
|
|
|
@@ -994,7 +977,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
994
977
|
}
|
|
995
978
|
|
|
996
979
|
// Mark this slot as attempted
|
|
997
|
-
this.
|
|
980
|
+
this.lastSlotForFallbackAction = slot;
|
|
998
981
|
|
|
999
982
|
// Get a publisher for voting
|
|
1000
983
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
@@ -1019,21 +1002,42 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1019
1002
|
const votesPromises = voter.enqueueVotes();
|
|
1020
1003
|
const votes = await Promise.all(votesPromises);
|
|
1021
1004
|
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
1005
|
+
// Even if we cannot build, try to prune so a stuck pending chain (e.g. bad data blocking sync) can
|
|
1006
|
+
// recover. prune() is permissionless, so it rides the same fallback multicall as the votes.
|
|
1007
|
+
const pruneEnqueued = await this.tryEnqueuePruneIfPrunable(targetSlot, publisher);
|
|
1008
|
+
|
|
1009
|
+
// Bail if nothing to do
|
|
1010
|
+
if (votes.every(p => !p) && !pruneEnqueued) {
|
|
1011
|
+
this.log.debug(`Nothing to enqueue for slot ${slot} (no votes, not prunable)`);
|
|
1024
1012
|
return;
|
|
1025
1013
|
}
|
|
1026
1014
|
|
|
1027
|
-
|
|
1015
|
+
const [governanceVoteEnqueued, slashingVoteEnqueued] = votes;
|
|
1016
|
+
this.log.info(`Submitting fallback requests in slot ${slot} despite sync failure`, {
|
|
1017
|
+
slot,
|
|
1018
|
+
pruneEnqueued,
|
|
1019
|
+
governanceVoteEnqueued: !!governanceVoteEnqueued,
|
|
1020
|
+
slashingVoteEnqueued: !!slashingVoteEnqueued,
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1028
1023
|
// Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
|
|
1029
1024
|
// expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
|
|
1030
1025
|
// slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
|
|
1031
1026
|
// work loop while waiting for the target slot to start.
|
|
1032
1027
|
void publisher.sendRequestsAt(targetSlot).catch(err => {
|
|
1033
|
-
this.log.error(`Failed to publish
|
|
1028
|
+
this.log.error(`Failed to publish fallback requests despite sync failure for slot ${slot}`, err, { slot });
|
|
1034
1029
|
});
|
|
1035
1030
|
}
|
|
1036
1031
|
|
|
1032
|
+
private async tryEnqueuePruneIfPrunable(targetSlot: SlotNumber, publisher: SequencerPublisher): Promise<boolean> {
|
|
1033
|
+
try {
|
|
1034
|
+
return await publisher.enqueuePruneIfPrunable(targetSlot);
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
this.log.error(`Failed to enqueue rollup prune for slot ${targetSlot}`, err, { targetSlot });
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1037
1041
|
/**
|
|
1038
1042
|
* Tries to vote on slashing actions and governance proposals when escape hatch is open.
|
|
1039
1043
|
* This allows the sequencer to participate in voting without performing checkpoint proposal work.
|
|
@@ -1047,13 +1051,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1047
1051
|
const { slot, targetSlot, proposer } = args;
|
|
1048
1052
|
|
|
1049
1053
|
// Prevent duplicate attempts in the same slot
|
|
1050
|
-
if (this.
|
|
1054
|
+
if (this.lastSlotForFallbackAction === slot) {
|
|
1051
1055
|
this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
|
|
1052
1056
|
return;
|
|
1053
1057
|
}
|
|
1054
1058
|
|
|
1055
1059
|
// Mark this slot as attempted
|
|
1056
|
-
this.
|
|
1060
|
+
this.lastSlotForFallbackAction = slot;
|
|
1057
1061
|
|
|
1058
1062
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
1059
1063
|
|
|
@@ -1092,7 +1096,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
1092
1096
|
// the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
|
|
1093
1097
|
// `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
|
|
1094
1098
|
// silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
|
|
1095
|
-
// waiting for the target slot to start, mirroring
|
|
1099
|
+
// waiting for the target slot to start, mirroring tryVoteAndPruneWhenCannotBuild.
|
|
1096
1100
|
void publisher.sendRequestsAt(targetSlot).catch(err => {
|
|
1097
1101
|
this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, { slot, targetSlot });
|
|
1098
1102
|
});
|
package/src/test/utils.ts
CHANGED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { type FeeHeader, RollupContract, type SimulationOverridesPlan } from '@aztec/ethereum/contracts';
|
|
2
|
-
import { CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
3
|
-
import type { Logger } from '@aztec/foundation/log';
|
|
4
|
-
import { type ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
|
|
5
|
-
import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
6
|
-
type CheckpointSimulationOverridesPlanInput = {
|
|
7
|
-
/** Target rollup contract. */
|
|
8
|
-
rollup: RollupContract;
|
|
9
|
-
/** Checkpoint number to be proposed. */
|
|
10
|
-
checkpointNumber: CheckpointNumber;
|
|
11
|
-
/** Logger instance. */
|
|
12
|
-
log: Logger;
|
|
13
|
-
/**
|
|
14
|
-
* The proposed parent checkpoint when pipelining. Its `checkpointNumber` must equal
|
|
15
|
-
* `checkpointNumber - 1`; the helper enforces this. Mutually exclusive with
|
|
16
|
-
* `invalidateToPendingCheckpointNumber`.
|
|
17
|
-
*/
|
|
18
|
-
proposedCheckpointData?: ProposedCheckpointData;
|
|
19
|
-
/**
|
|
20
|
-
* The pending checkpoint number we'll end up at after invalidation lands. Mutually exclusive
|
|
21
|
-
* with `proposedCheckpointData`.
|
|
22
|
-
*/
|
|
23
|
-
invalidateToPendingCheckpointNumber?: CheckpointNumber;
|
|
24
|
-
/**
|
|
25
|
-
* The real on-chain pending checkpoint number (typically `syncedTo.checkpointedCheckpointNumber`).
|
|
26
|
-
* Used as the snapshot we pin both `pending` and `proven` to avoid prunes in simulation.
|
|
27
|
-
*/
|
|
28
|
-
checkpointedCheckpointNumber: CheckpointNumber;
|
|
29
|
-
/**
|
|
30
|
-
* Chain-level consensus signature context. Used to recompute the parent's `payloadDigest` for the
|
|
31
|
-
* pipelined simulation override so it matches what `propose` will write into `tempCheckpointLogs[parent]`
|
|
32
|
-
* once the parent lands.
|
|
33
|
-
*/
|
|
34
|
-
signatureContext: CoordinationSignatureContext;
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Builds the SimulationOverridesPlan describing the simulated L1 rollup state for a checkpoint's
|
|
38
|
-
* enqueue-time simulations: `canProposeAt` (in Sequencer.doWork) and the propose-related sims
|
|
39
|
-
* (validateBlockHeader, simulateProposeTx). The plan reflects "as if our pipelined parent
|
|
40
|
-
* checkpoint has landed and any required invalidation has executed" — the gap that needs to be
|
|
41
|
-
* bridged at enqueue time.
|
|
42
|
-
*
|
|
43
|
-
* Pipelining (`proposedCheckpointData`) and invalidation (`invalidateToPendingCheckpointNumber`)
|
|
44
|
-
* are mutually exclusive; passing both throws.
|
|
45
|
-
*/
|
|
46
|
-
export declare function buildCheckpointSimulationOverridesPlan(input: CheckpointSimulationOverridesPlanInput): Promise<SimulationOverridesPlan | undefined>;
|
|
47
|
-
type PipelinedParentFeeHeaderInput = {
|
|
48
|
-
checkpointNumber: CheckpointNumber;
|
|
49
|
-
proposedCheckpointData: ProposedCheckpointData;
|
|
50
|
-
rollup: RollupContract;
|
|
51
|
-
log: Logger;
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Derives the pending parent fee header used during pipelined proposal simulation. Returns
|
|
55
|
-
* `undefined` only when no grandparent exists (i.e. the proposed parent is the genesis
|
|
56
|
-
* checkpoint); all other failure modes (missing grandparent state, missing fee header, RPC
|
|
57
|
-
* errors) throw so callers don't silently desync the fee-header override.
|
|
58
|
-
*/
|
|
59
|
-
export declare function computePipelinedParentFeeHeader(input: PipelinedParentFeeHeaderInput): Promise<FeeHeader | undefined>;
|
|
60
|
-
export {};
|
|
61
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhaW5fc3RhdGVfb3ZlcnJpZGVzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2VxdWVuY2VyL2NoYWluX3N0YXRlX292ZXJyaWRlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxTQUFTLEVBQ2QsY0FBYyxFQUVkLEtBQUssdUJBQXVCLEVBQzdCLE1BQU0sMkJBQTJCLENBQUM7QUFDbkMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDbkUsT0FBTyxLQUFLLEVBQUUsTUFBTSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDcEQsT0FBTyxFQUFFLEtBQUssc0JBQXNCLEVBQWtDLE1BQU0sMEJBQTBCLENBQUM7QUFDdkcsT0FBTyxLQUFLLEVBQUUsNEJBQTRCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUV0RSxLQUFLLHNDQUFzQyxHQUFHO0lBQzVDLDhCQUE4QjtJQUM5QixNQUFNLEVBQUUsY0FBYyxDQUFDO0lBQ3ZCLHdDQUF3QztJQUN4QyxnQkFBZ0IsRUFBRSxnQkFBZ0IsQ0FBQztJQUNuQyx1QkFBdUI7SUFDdkIsR0FBRyxFQUFFLE1BQU0sQ0FBQztJQUNaOzs7O09BSUc7SUFDSCxzQkFBc0IsQ0FBQyxFQUFFLHNCQUFzQixDQUFDO0lBQ2hEOzs7T0FHRztJQUNILG1DQUFtQyxDQUFDLEVBQUUsZ0JBQWdCLENBQUM7SUFDdkQ7OztPQUdHO0lBQ0gsNEJBQTRCLEVBQUUsZ0JBQWdCLENBQUM7SUFDL0M7Ozs7T0FJRztJQUNILGdCQUFnQixFQUFFLDRCQUE0QixDQUFDO0NBQ2hELENBQUM7QUFFRjs7Ozs7Ozs7O0dBU0c7QUFDSCx3QkFBc0Isc0NBQXNDLENBQzFELEtBQUssRUFBRSxzQ0FBc0MsR0FDNUMsT0FBTyxDQUFDLHVCQUF1QixHQUFHLFNBQVMsQ0FBQyxDQXNEOUM7QUFxQkQsS0FBSyw2QkFBNkIsR0FBRztJQUNuQyxnQkFBZ0IsRUFBRSxnQkFBZ0IsQ0FBQztJQUNuQyxzQkFBc0IsRUFBRSxzQkFBc0IsQ0FBQztJQUMvQyxNQUFNLEVBQUUsY0FBYyxDQUFDO0lBQ3ZCLEdBQUcsRUFBRSxNQUFNLENBQUM7Q0FDYixDQUFDO0FBRUY7Ozs7O0dBS0c7QUFDSCx3QkFBc0IsK0JBQStCLENBQ25ELEtBQUssRUFBRSw2QkFBNkIsR0FDbkMsT0FBTyxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUMsQ0F3QmhDIn0=
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chain_state_overrides.d.ts","sourceRoot":"","sources":["../../src/sequencer/chain_state_overrides.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,cAAc,EAEd,KAAK,uBAAuB,EAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,KAAK,sBAAsB,EAAkC,MAAM,0BAA0B,CAAC;AACvG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAEtE,KAAK,sCAAsC,GAAG;IAC5C,8BAA8B;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,wCAAwC;IACxC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD;;;OAGG;IACH,mCAAmC,CAAC,EAAE,gBAAgB,CAAC;IACvD;;;OAGG;IACH,4BAA4B,EAAE,gBAAgB,CAAC;IAC/C;;;;OAIG;IACH,gBAAgB,EAAE,4BAA4B,CAAC;CAChD,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAsB,sCAAsC,CAC1D,KAAK,EAAE,sCAAsC,GAC5C,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CAsD9C;AAqBD,KAAK,6BAA6B,GAAG;IACnC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,sBAAsB,EAAE,sBAAsB,CAAC;IAC/C,MAAM,EAAE,cAAc,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAwBhC"}
|