@aztec/sequencer-client 0.0.1-commit.2f68f620 → 0.0.1-commit.3100065
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/README.md +27 -27
- package/dest/client/sequencer-client.d.ts +16 -3
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +17 -12
- package/dest/config.d.ts +6 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -14
- package/dest/global_variable_builder/fee_predictor.d.ts +1 -1
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -1
- package/dest/global_variable_builder/fee_predictor.js +11 -1
- package/dest/global_variable_builder/fee_provider.d.ts +1 -1
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -1
- package/dest/global_variable_builder/fee_provider.js +27 -5
- 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/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 +5 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +11 -1
- package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
- package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
- package/dest/publisher/l1_to_l2_messaging.js +70 -0
- 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 +119 -22
- package/dest/publisher/write_json.d.ts +11 -0
- package/dest/publisher/write_json.d.ts.map +1 -0
- package/dest/publisher/write_json.js +57 -0
- package/dest/sequencer/automine/automine_factory.d.ts +5 -3
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_factory.js +2 -1
- package/dest/sequencer/automine/automine_sequencer.d.ts +43 -5
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
- package/dest/sequencer/automine/automine_sequencer.js +199 -18
- package/dest/sequencer/automine/index.d.ts +3 -0
- package/dest/sequencer/automine/index.d.ts.map +1 -0
- package/dest/sequencer/automine/index.js +2 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -16
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +98 -90
- package/dest/sequencer/errors.d.ts +1 -8
- package/dest/sequencer/errors.d.ts.map +1 -1
- package/dest/sequencer/errors.js +0 -9
- package/dest/sequencer/events.d.ts +16 -5
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -3
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -2
- package/dest/sequencer/requests_tracker.d.ts +22 -0
- package/dest/sequencer/requests_tracker.d.ts.map +1 -0
- package/dest/sequencer/requests_tracker.js +33 -0
- package/dest/sequencer/sequencer.d.ts +110 -36
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +329 -174
- package/dest/test/index.d.ts +3 -3
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/utils.d.ts +15 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +18 -1
- package/package.json +28 -27
- package/src/client/sequencer-client.ts +20 -13
- package/src/config.ts +25 -12
- package/src/global_variable_builder/fee_predictor.ts +11 -1
- package/src/global_variable_builder/fee_provider.ts +27 -5
- package/src/global_variable_builder/global_builder.ts +2 -34
- package/src/index.ts +1 -10
- package/src/publisher/config.ts +22 -1
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/sequencer-publisher.ts +116 -23
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/automine/README.md +18 -4
- package/src/sequencer/automine/automine_factory.ts +8 -1
- package/src/sequencer/automine/automine_sequencer.ts +221 -20
- package/src/sequencer/automine/index.ts +6 -0
- package/src/sequencer/checkpoint_proposal_job.ts +119 -100
- package/src/sequencer/errors.ts +0 -15
- package/src/sequencer/events.ts +16 -4
- package/src/sequencer/index.ts +0 -2
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +360 -187
- package/src/test/index.ts +2 -2
- package/src/test/utils.ts +33 -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/dest/sequencer/timetable.d.ts +0 -98
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -227
- package/src/sequencer/README.md +0 -243
- package/src/sequencer/chain_state_overrides.ts +0 -162
- package/src/sequencer/timetable.ts +0 -288
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
2
|
+
import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
|
|
3
|
+
import type { ExtendedViemWalletClient } from '@aztec/ethereum/types';
|
|
4
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
5
|
+
import { tryJsonStringify } from '@aztec/foundation/json-rpc';
|
|
6
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
7
|
+
import { InboxAbi } from '@aztec/l1-artifacts';
|
|
8
|
+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
9
|
+
|
|
10
|
+
import { decodeEventLog, getContract } from 'viem';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sends an L1-to-L2 message via the Inbox contract and returns its hash and global leaf index.
|
|
14
|
+
* Test helper copied from the end-to-end fixtures so this integration test can live in sequencer-client.
|
|
15
|
+
*/
|
|
16
|
+
export async function sendL1ToL2Message(
|
|
17
|
+
message: { recipient: AztecAddress; content: Fr; secretHash: Fr },
|
|
18
|
+
ctx: {
|
|
19
|
+
l1Client: ExtendedViemWalletClient;
|
|
20
|
+
l1ContractAddresses: Pick<L1ContractAddresses, 'inboxAddress' | 'rollupAddress'>;
|
|
21
|
+
},
|
|
22
|
+
) {
|
|
23
|
+
const logger = createLogger('sequencer-client:l1_to_l2_messaging');
|
|
24
|
+
const inbox = getContract({
|
|
25
|
+
address: ctx.l1ContractAddresses.inboxAddress.toString(),
|
|
26
|
+
abi: InboxAbi,
|
|
27
|
+
client: ctx.l1Client,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { recipient, content, secretHash } = message;
|
|
31
|
+
|
|
32
|
+
const version = await new RollupContract(ctx.l1Client, ctx.l1ContractAddresses.rollupAddress.toString()).getVersion();
|
|
33
|
+
|
|
34
|
+
// We inject the message to Inbox
|
|
35
|
+
const txHash = await inbox.write.sendL2Message(
|
|
36
|
+
[{ actor: recipient.toString(), version: BigInt(version) }, content.toString(), secretHash.toString()],
|
|
37
|
+
{
|
|
38
|
+
gas: 1_000_000n,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
logger.info(`L1 to L2 message sent in tx ${txHash}`);
|
|
42
|
+
|
|
43
|
+
// We check that the message was correctly injected by checking the emitted event
|
|
44
|
+
const txReceipt = await ctx.l1Client.waitForTransactionReceipt({ hash: txHash });
|
|
45
|
+
|
|
46
|
+
if (txReceipt.status !== 'success') {
|
|
47
|
+
throw new Error(`L1 to L2 message failed to be sent in tx ${txHash}. Status: ${txReceipt.status}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logger.info(`L1 to L2 message receipt retrieved for tx ${txReceipt.transactionHash}`, txReceipt);
|
|
51
|
+
|
|
52
|
+
if (txReceipt.transactionHash !== txHash) {
|
|
53
|
+
throw new Error(`Receipt transaction hash mismatch: ${txReceipt.transactionHash} !== ${txHash}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Filter for MessageSent events from the Inbox contract by trying to decode each log
|
|
57
|
+
const messageSentLogs = txReceipt.logs
|
|
58
|
+
.filter(log => log.address.toLowerCase() === ctx.l1ContractAddresses.inboxAddress.toString().toLowerCase())
|
|
59
|
+
.map(log => {
|
|
60
|
+
try {
|
|
61
|
+
const decoded = decodeEventLog({
|
|
62
|
+
abi: InboxAbi,
|
|
63
|
+
data: log.data,
|
|
64
|
+
topics: log.topics,
|
|
65
|
+
});
|
|
66
|
+
return { log, decoded };
|
|
67
|
+
} catch {
|
|
68
|
+
return null; // Not a decodable event from this ABI
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
.filter((item): item is { log: any; decoded: any } => item !== null && item.decoded.eventName === 'MessageSent');
|
|
72
|
+
|
|
73
|
+
if (messageSentLogs.length !== 1) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Wrong number of MessageSent logs found in ${txHash} transaction (got ${messageSentLogs.length} expected 1)\n${tryJsonStringify(messageSentLogs.map(item => item.log))}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// We already have the decoded event
|
|
80
|
+
const topics = messageSentLogs[0].decoded;
|
|
81
|
+
const receivedMsgHash = topics.args.hash;
|
|
82
|
+
const receivedGlobalLeafIndex = topics.args.index;
|
|
83
|
+
|
|
84
|
+
return { msgHash: Fr.fromHexString(receivedMsgHash), globalLeafIndex: new Fr(receivedGlobalLeafIndex), txReceipt };
|
|
85
|
+
}
|
|
@@ -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;
|
|
@@ -458,7 +469,9 @@ export class SequencerPublisher {
|
|
|
458
469
|
|
|
459
470
|
if (bundleResult.kind === 'aborted') {
|
|
460
471
|
this.logDroppedInSim(bundleResult.droppedRequests);
|
|
461
|
-
void this.backupDroppedInSim(bundleResult.droppedRequests)
|
|
472
|
+
void this.backupDroppedInSim(bundleResult.droppedRequests).catch(err =>
|
|
473
|
+
this.log.error(`Failed to backup requests dropped in simulation`, err),
|
|
474
|
+
);
|
|
462
475
|
return undefined;
|
|
463
476
|
}
|
|
464
477
|
|
|
@@ -617,34 +630,69 @@ export class SequencerPublisher {
|
|
|
617
630
|
|
|
618
631
|
/*
|
|
619
632
|
* 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
633
|
*/
|
|
626
634
|
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;
|
|
635
|
+
await this.waitForTargetSlot(targetSlot);
|
|
633
636
|
if (this.interrupted) {
|
|
634
637
|
return undefined;
|
|
635
638
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
639
|
+
|
|
640
|
+
return this.sendRequests(targetSlot);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Sleeps until one L1 slot before the L2 slot boundary, and then waits for that L1 block
|
|
645
|
+
* to be mined, so we don't risk being included in it. If that block never gets mined after
|
|
646
|
+
* a timeout, we assume it got skipped on L1, so we send the tx anyway.
|
|
647
|
+
*/
|
|
648
|
+
private async waitForTargetSlot(targetSlot: SlotNumber): Promise<void> {
|
|
649
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
650
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
651
|
+
const startOfTargetSlotTs = getTimestampForSlot(targetSlot, l1Constants);
|
|
652
|
+
const previousL1BlockTs = startOfTargetSlotTs - this.ethereumSlotDuration;
|
|
653
|
+
const waitDeadlineTs = previousL1BlockTs + BigInt(this.previousL1BlockWaitTimeoutMs / 1000);
|
|
654
|
+
const logCtx = { targetSlot, startOfTargetSlotTs, nowInSeconds, previousL1BlockTs, waitDeadlineTs };
|
|
655
|
+
|
|
656
|
+
// Check if we are already past time
|
|
657
|
+
if (nowInSeconds >= startOfTargetSlotTs) {
|
|
658
|
+
this.log.verbose(`Target slot ${targetSlot} already started, sending requests immediately`, logCtx);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Otherwise we wait
|
|
663
|
+
this.log.debug(`Waiting for slot ${targetSlot} before sending requests`, logCtx);
|
|
664
|
+
|
|
665
|
+
// Wait until previous L1 block timestamp first
|
|
666
|
+
const sleepMs = (Number(previousL1BlockTs) - nowInSeconds) * 1000;
|
|
667
|
+
if (sleepMs > 0 && !this.interrupted) {
|
|
668
|
+
this.log.trace(`Sleeping ${sleepMs}ms before waiting for previous L1 block`, logCtx);
|
|
642
669
|
await this.interruptibleSleep.sleep(sleepMs);
|
|
643
670
|
}
|
|
644
|
-
|
|
645
|
-
|
|
671
|
+
|
|
672
|
+
// Then loop until we see the previous L1 block, so we know that we cannot be included in it.
|
|
673
|
+
// We time out after a while, once we are sure that that block is skipped in L1.
|
|
674
|
+
while (!this.interrupted) {
|
|
675
|
+
try {
|
|
676
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
677
|
+
logCtx.nowInSeconds = nowInSeconds;
|
|
678
|
+
|
|
679
|
+
if (nowInSeconds >= waitDeadlineTs) {
|
|
680
|
+
this.log.warn(`Timed out waiting for previous L1 block before sending requests, proceeding`, logCtx);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const latestBlockTs = await this.l1TxUtils.getBlock().then(b => b.timestamp);
|
|
685
|
+
if (latestBlockTs >= previousL1BlockTs) {
|
|
686
|
+
this.log.debug(`Previous L1 block mined, proceeding to send requests`, { ...logCtx, latestBlockTs });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
this.log.trace(`Previous L1 block not mined yet, continuing to wait`, { ...logCtx, latestBlockTs });
|
|
690
|
+
} catch (err) {
|
|
691
|
+
this.log.error(`Error while waiting for previous L1 block before sending requests; retrying`, err, logCtx);
|
|
692
|
+
} finally {
|
|
693
|
+
await this.interruptibleSleep.sleep(this.previousL1BlockWaitPollIntervalMs);
|
|
694
|
+
}
|
|
646
695
|
}
|
|
647
|
-
return this.sendRequests(targetSlot);
|
|
648
696
|
}
|
|
649
697
|
|
|
650
698
|
private callbackBundledTransactions(
|
|
@@ -866,7 +914,11 @@ export class SequencerPublisher {
|
|
|
866
914
|
const logData = { ...checkpoint, reason };
|
|
867
915
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
868
916
|
|
|
869
|
-
|
|
917
|
+
// Use the exact packed tuple posted to L1 verbatim. A repack via `packAttestations` is not a
|
|
918
|
+
// byte-faithful inverse of `fromPacked` (a canonicalized yParity byte or an all-zero signature slot
|
|
919
|
+
// round-trips differently), so it would diverge from the stored `attestationsHash` and revert the
|
|
920
|
+
// invalidation.
|
|
921
|
+
const attestationsAndSigners = validationResult.verbatimAttestations;
|
|
870
922
|
|
|
871
923
|
if (reason === 'invalid-attestation') {
|
|
872
924
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -1015,6 +1067,47 @@ export class SequencerPublisher {
|
|
|
1015
1067
|
);
|
|
1016
1068
|
}
|
|
1017
1069
|
|
|
1070
|
+
/**
|
|
1071
|
+
* Enqueues a `prune()` transaction if the rollup is prunable at the given slot's L1 timestamp.
|
|
1072
|
+
* `prune()` is permissionless and idempotent — if the chain is no longer prunable by send time the
|
|
1073
|
+
* bundle simulation usually drops the entry; on a node without `eth_simulateV1` the bundle is sent
|
|
1074
|
+
* as-is and the prune reverts `Rollup__NothingToPrune` inside `aggregate3(allowFailure: true)`
|
|
1075
|
+
* (a failed action, never a whole-tx revert). Used by the failed-sync fallback so a stuck pending
|
|
1076
|
+
* chain (e.g. bad data blocking sync) can be wound back to recover.
|
|
1077
|
+
* @returns true if a prune request was enqueued, false otherwise.
|
|
1078
|
+
*/
|
|
1079
|
+
public async enqueuePruneIfPrunable(slotNumber: SlotNumber): Promise<boolean> {
|
|
1080
|
+
if (this.lastActions['prune'] === slotNumber) {
|
|
1081
|
+
this.log.debug(`Skipping duplicate prune for slot ${slotNumber}`, { slotNumber });
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
// Use the SAME timestamp the bundle simulator overrides block.timestamp with at send time
|
|
1085
|
+
// (sequencer-bundle-simulator.ts) so this upfront check and the send-time sim agree. Slot-start
|
|
1086
|
+
// and last-L1-slot both fall within the same L2 slot (and epoch, which is what `canPruneAtTime`
|
|
1087
|
+
// derives), so they agree today; matching the simulator keeps it robust if the contract ever uses
|
|
1088
|
+
// the timestamp more granularly.
|
|
1089
|
+
const ts = getLastL1SlotTimestampForL2Slot(slotNumber, this.epochCache.getL1Constants());
|
|
1090
|
+
const canPrune = await this.rollupContract.canPruneAtTime(ts).catch(err => {
|
|
1091
|
+
this.log.error(`Failed to check canPruneAtTime for slot ${slotNumber}`, err, { slotNumber });
|
|
1092
|
+
return false;
|
|
1093
|
+
});
|
|
1094
|
+
if (!canPrune) {
|
|
1095
|
+
this.log.debug(`Rollup not prunable at slot ${slotNumber}`, { slotNumber });
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
const request: L1TxRequest = {
|
|
1099
|
+
to: this.rollupContract.address,
|
|
1100
|
+
data: encodeFunctionData({ abi: RollupAbi, functionName: 'prune', args: [] }),
|
|
1101
|
+
};
|
|
1102
|
+
this.log.info(`Enqueuing rollup prune for slot ${slotNumber}`, { slotNumber });
|
|
1103
|
+
return this.enqueueRequest(
|
|
1104
|
+
'prune',
|
|
1105
|
+
request,
|
|
1106
|
+
{ address: this.rollupContract.address, abi: RollupAbi, eventName: 'PrunedPending' },
|
|
1107
|
+
slotNumber,
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1018
1111
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
1019
1112
|
public async enqueueSlashingActions(
|
|
1020
1113
|
actions: ProposerSlashAction[],
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AztecAddress } from '@aztec/aztec.js/addresses';
|
|
2
|
+
import { Fr } from '@aztec/aztec.js/fields';
|
|
3
|
+
import { BatchedBlob, Blob, getEthBlobEvaluationInputs, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
4
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
|
+
import { L2Block } from '@aztec/stdlib/block';
|
|
6
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
7
|
+
|
|
8
|
+
import { writeFile } from 'fs/promises';
|
|
9
|
+
|
|
10
|
+
const AZTEC_GENERATE_TEST_DATA = !!process.env.AZTEC_GENERATE_TEST_DATA;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a json object that can be used to test the solidity contract.
|
|
14
|
+
* The json object must be put into
|
|
15
|
+
*/
|
|
16
|
+
export async function writeJson(
|
|
17
|
+
fileName: string,
|
|
18
|
+
checkpointHeader: CheckpointHeader,
|
|
19
|
+
block: L2Block,
|
|
20
|
+
l1ToL2Content: Fr[],
|
|
21
|
+
blobs: Blob[],
|
|
22
|
+
batchedBlob: BatchedBlob,
|
|
23
|
+
recipientAddress: AztecAddress,
|
|
24
|
+
deployerAddress: `0x${string}`,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
if (!AZTEC_GENERATE_TEST_DATA) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Path relative to the package.json in the end-to-end folder
|
|
30
|
+
const path = `../../l1-contracts/test/fixtures/${fileName}.json`;
|
|
31
|
+
|
|
32
|
+
const asHex = (value: Fr | Buffer | EthAddress | AztecAddress, size = 64) => {
|
|
33
|
+
const buffer = Buffer.isBuffer(value) ? value : value.toBuffer();
|
|
34
|
+
return `0x${buffer.toString('hex').padStart(size, '0')}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const jsonObject = {
|
|
38
|
+
populate: {
|
|
39
|
+
l1ToL2Content: l1ToL2Content.map(value => asHex(value)),
|
|
40
|
+
recipient: asHex(recipientAddress.toField()),
|
|
41
|
+
sender: deployerAddress,
|
|
42
|
+
},
|
|
43
|
+
messages: {
|
|
44
|
+
l2ToL1Messages: block.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs).map(value => asHex(value)),
|
|
45
|
+
},
|
|
46
|
+
checkpoint: {
|
|
47
|
+
// The json formatting in forge is a bit brittle, so we convert Fr to a number in the few values below.
|
|
48
|
+
// This should not be a problem for testing as long as the values are not larger than u32.
|
|
49
|
+
archive: asHex(block.archive.root),
|
|
50
|
+
blobCommitments: getPrefixedEthBlobCommitments(blobs),
|
|
51
|
+
batchedBlobInputs: getEthBlobEvaluationInputs(batchedBlob),
|
|
52
|
+
checkpointNumber: block.number,
|
|
53
|
+
body: `0x${block.body.toBuffer().toString('hex')}`,
|
|
54
|
+
header: {
|
|
55
|
+
lastArchiveRoot: asHex(checkpointHeader.lastArchiveRoot),
|
|
56
|
+
blockHeadersHash: asHex(checkpointHeader.blockHeadersHash),
|
|
57
|
+
blobsHash: asHex(checkpointHeader.blobsHash),
|
|
58
|
+
inHash: asHex(checkpointHeader.inHash),
|
|
59
|
+
outHash: asHex(checkpointHeader.epochOutHash),
|
|
60
|
+
slotNumber: Number(checkpointHeader.slotNumber),
|
|
61
|
+
timestamp: Number(checkpointHeader.timestamp),
|
|
62
|
+
coinbase: asHex(checkpointHeader.coinbase, 40),
|
|
63
|
+
feeRecipient: asHex(checkpointHeader.feeRecipient),
|
|
64
|
+
gasFees: {
|
|
65
|
+
feePerDaGas: Number(checkpointHeader.gasFees.feePerDaGas),
|
|
66
|
+
feePerL2Gas: Number(checkpointHeader.gasFees.feePerL2Gas),
|
|
67
|
+
},
|
|
68
|
+
totalManaUsed: checkpointHeader.totalManaUsed.toNumber(),
|
|
69
|
+
accumulatedFees: checkpointHeader.accumulatedFees.toNumber(),
|
|
70
|
+
},
|
|
71
|
+
headerHash: asHex(checkpointHeader.hash()),
|
|
72
|
+
numTxs: block.body.txEffects.length,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const output = JSON.stringify(jsonObject, null, 2);
|
|
77
|
+
await writeFile(path, output, 'utf8');
|
|
78
|
+
}
|
|
@@ -19,11 +19,11 @@ Compared to the production `Sequencer`:
|
|
|
19
19
|
- No validator orchestration, attestation collection, or P2P proposal gossip.
|
|
20
20
|
- No slashing, no governance votes, no `SequencerEvents`.
|
|
21
21
|
|
|
22
|
-
Consumers (archiver, world-state
|
|
22
|
+
Consumers (archiver, world-state) observe L1 and the archiver tip directly rather than listening for sequencer events.
|
|
23
23
|
|
|
24
24
|
## Serial-queue invariant
|
|
25
25
|
|
|
26
|
-
Every operation — mempool-driven block builds, explicit empty-block builds, time warps, and
|
|
26
|
+
Every operation — mempool-driven block builds, explicit empty-block builds, time warps, reorgs, and synthetic epoch proving — is serialized through a single `SerialQueue`. They never interleave.
|
|
27
27
|
|
|
28
28
|
Public entry points:
|
|
29
29
|
|
|
@@ -32,15 +32,29 @@ Public entry points:
|
|
|
32
32
|
| `buildIfPending()` | Enqueues a mempool-driven build. Coalesces bursts into one job. |
|
|
33
33
|
| `buildEmptyBlock()` | Enqueues a forced empty-block build. |
|
|
34
34
|
| `warpTo(ts)` / `warpBy(delta)` | Advances L1 time to a slot boundary. |
|
|
35
|
+
| `prove(upToCheckpoint?)` | Synthetically proves epochs up to a checkpoint (default: the latest checkpointed): writes the epoch out hashes into the L1 Outbox so L2-to-L1 messages become consumable, then advances the proven tip. No real proof. Clamps to the checkpointed tip and no-ops when already proven. |
|
|
35
36
|
| `revertToCheckpoint(n)` | Rolls L1 back to the block that published checkpoint `n`, then resets archiver, world-state, and P2P pool. |
|
|
36
37
|
| `syncPoint()` | Awaits the queue reaching idle. |
|
|
37
38
|
|
|
39
|
+
## Time control
|
|
40
|
+
|
|
41
|
+
The AutomineSequencer owns L1 time in the local network (replacing the deleted `AnvilTestWatcher`). It builds and publishes each checkpoint at the next aztec-slot boundary, and `warpTo` / `warpBy` advance the clock by publishing an empty checkpoint at the target slot. Before every build, warp, and prove it reconciles the injected `TestDateProvider` to the latest *mined* L1 timestamp, so node-side consumers of `dateProvider.now()` stay aligned with L1 even when an unrelated L1 tx mines a block between our builds. It never advances the clock to the pending, un-mined timestamp.
|
|
42
|
+
|
|
43
|
+
## Publish-failure recovery
|
|
44
|
+
|
|
45
|
+
A failed propose mines no checkpoint on L1 (it reverts inside the multicall or is never sent), so recovery is purely local — there is **no L1 reorg**. The optimistic archiver insert (the proposed block plus its proposed checkpoint) is rolled back via `archiver.removeUncheckpointedBlocksAfter`, which removes the uncheckpointed block and evicts the proposed checkpoint that referenced it. `p2pClient.sync()` then observes the lowered proposed tip and returns the block's txs to the pending pool, `worldState.syncImmediate()` drops any applied effects, and the L1 nonce is reset. The build is not retried inline; the mempool poller re-enqueues one once the txs are back in the pool.
|
|
46
|
+
|
|
38
47
|
## Entry points
|
|
39
48
|
|
|
40
49
|
**Factory** — `createAutomineSequencer` in `automine_factory.ts` wires up all dependencies (publisher manager, keystore, cheat codes, etc.), starts the `PublisherManager`, and returns an unstarted `AutomineSequencer`. The caller (`AztecNodeService.createAndSync` in `aztec-node/src/aztec-node/server.ts`) invokes `AutomineSequencer.start()` separately. It is called by the full node when `aztecTargetCommitteeSize == 0`.
|
|
41
50
|
|
|
42
51
|
**Test fixture** — `AUTOMINE_E2E_OPTS` in `end-to-end/src/fixtures/fixtures.ts` is the test-side entry point. Pass it to `setup()` to get a node + `AutomineSequencer` instead of the production sequencer stack.
|
|
43
52
|
|
|
44
|
-
## Epoch proving
|
|
53
|
+
## Epoch proving
|
|
54
|
+
|
|
55
|
+
There is no real prover in the automine setup, so epochs are settled synthetically: the epoch out hash is written into the L1 Outbox via cheat codes and the rollup's proven tip is advanced — the local-network equivalent of an epoch proof landing on L1. The grouping/out-hash logic lives in the shared `settleEpochOutbox` helper in `@aztec/prover-client/test`, used by both proving drivers below.
|
|
56
|
+
|
|
57
|
+
Who drives proving depends on the `automineEnableProveEpoch` config flag:
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
- **Local network / sandbox** (`automineEnableProveEpoch: true`): the AutomineSequencer runs an auto-prove loop that calls `prove()` as checkpoints land, through the same serial queue as its builds. This replaces the standalone `EpochTestSettler`, which used to race the build loop. The loop also reconciles the clock on each tick.
|
|
60
|
+
- **e2e tests** (`AUTOMINE_E2E_OPTS`, flag off): proving is manual so tests stay deterministic. Cross-epoch tests advance the proven anchor explicitly via `node.prove(...)`, `cheatCodes.rollup.markAsProven(...)`, or a hand-driven `EpochTestSettler`. See `e2e_pruned_blocks.test.ts` and `e2e_epochs/epochs_partial_proof_multi_root.test.ts` for real examples.
|
|
@@ -40,9 +40,14 @@ export type CreateAutomineSequencerArgs = {
|
|
|
40
40
|
worldStateSynchronizer: WorldStateSynchronizer;
|
|
41
41
|
archiver: L2BlockSource &
|
|
42
42
|
L1ToL2MessageSource &
|
|
43
|
-
Pick<
|
|
43
|
+
Pick<
|
|
44
|
+
Archiver,
|
|
45
|
+
'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate' | 'removeUncheckpointedBlocksAfter'
|
|
46
|
+
>;
|
|
44
47
|
p2pClient: P2P & Pick<ConcreteP2PClient, 'sync'>;
|
|
45
48
|
l1Constants: { l1GenesisTime: bigint; slotDuration: number; ethereumSlotDuration: number; rollupManaLimit: number };
|
|
49
|
+
/** When true, run the auto-settle / clock-reconcile loop (local-network only). */
|
|
50
|
+
autoSettle?: boolean;
|
|
46
51
|
log: Logger;
|
|
47
52
|
};
|
|
48
53
|
|
|
@@ -71,6 +76,7 @@ export async function createAutomineSequencer({
|
|
|
71
76
|
archiver,
|
|
72
77
|
p2pClient,
|
|
73
78
|
l1Constants,
|
|
79
|
+
autoSettle,
|
|
74
80
|
log,
|
|
75
81
|
}: CreateAutomineSequencerArgs): Promise<AutomineSequencer> {
|
|
76
82
|
const publisherManager = new PublisherManager(l1TxUtils, getPublisherConfigFromSequencerConfig(config), {
|
|
@@ -136,6 +142,7 @@ export async function createAutomineSequencer({
|
|
|
136
142
|
config,
|
|
137
143
|
archiver,
|
|
138
144
|
l1TxUtils: reorgResetL1TxUtils,
|
|
145
|
+
autoSettle,
|
|
139
146
|
stopExtras: () => publisherManager.stop(),
|
|
140
147
|
log: log.createChild('automine-sequencer'),
|
|
141
148
|
});
|