@aztec/sequencer-client 0.0.1-commit.b9865e97 → 0.0.1-commit.be03c316
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/l1_tx_failed_store/failed_tx_store.d.ts +48 -8
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +68 -1
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +2 -2
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +4 -2
- package/dest/publisher/l1_tx_failed_store/index.d.ts +2 -2
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/index.js +1 -0
- package/dest/publisher/sequencer-publisher.d.ts +34 -6
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +362 -61
- 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/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 +30 -28
- 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/l1_tx_failed_store/failed_tx_store.ts +114 -7
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +4 -3
- package/src/publisher/l1_tx_failed_store/index.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +383 -72
- 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/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/chain_state_overrides.ts +0 -162
- package/src/sequencer/timetable.ts +0 -288
|
@@ -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
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Archiver } from '@aztec/archiver';
|
|
2
2
|
import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
|
|
3
|
-
import type
|
|
4
|
-
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { type EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test';
|
|
4
|
+
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
5
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
6
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
7
7
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
@@ -10,6 +10,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
10
10
|
import type { TestDateProvider } from '@aztec/foundation/timer';
|
|
11
11
|
import { isErrorClass } from '@aztec/foundation/types';
|
|
12
12
|
import type { P2PClient as ConcreteP2PClient, P2P } from '@aztec/p2p';
|
|
13
|
+
import { settleEpochOutbox } from '@aztec/prover-client/test';
|
|
13
14
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
14
15
|
import { CommitteeAttestationsAndSigners, type L2Block, type L2BlockSource } from '@aztec/stdlib/block';
|
|
15
16
|
import { getPreviousCheckpointOutHashes } from '@aztec/stdlib/checkpoint';
|
|
@@ -65,7 +66,10 @@ export type AutomineSequencerDeps = {
|
|
|
65
66
|
* Archiver used to push locally-built blocks and proposed checkpoints into the in-memory
|
|
66
67
|
* store, force an immediate L1 sync after publishing, and roll back state during reorgs.
|
|
67
68
|
*/
|
|
68
|
-
archiver: Pick<
|
|
69
|
+
archiver: Pick<
|
|
70
|
+
Archiver,
|
|
71
|
+
'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate' | 'removeUncheckpointedBlocksAfter'
|
|
72
|
+
>;
|
|
69
73
|
/** L1 tx utils whose cached nonces must be reset after an L1 reorg. */
|
|
70
74
|
l1TxUtils: Pick<L1TxUtils, 'resetNonce'>[];
|
|
71
75
|
/**
|
|
@@ -75,6 +79,14 @@ export type AutomineSequencerDeps = {
|
|
|
75
79
|
stopExtras?: () => Promise<void>;
|
|
76
80
|
/** How often to poll the mempool for new txs while running. Defaults to 50ms. */
|
|
77
81
|
pollIntervalMs?: number;
|
|
82
|
+
/**
|
|
83
|
+
* When true, run a loop that synthetically settles epochs (writes outbox roots and advances the
|
|
84
|
+
* proven tip) as checkpoints land, replacing the standalone `EpochTestSettler`. Local-network only;
|
|
85
|
+
* e2e tests leave this off and drive proving explicitly.
|
|
86
|
+
*/
|
|
87
|
+
autoSettle?: boolean;
|
|
88
|
+
/** How often the auto-settle / clock-reconcile loop runs while {@link autoSettle} is enabled. Defaults to 200ms. */
|
|
89
|
+
settlePollIntervalMs?: number;
|
|
78
90
|
log?: Logger;
|
|
79
91
|
};
|
|
80
92
|
|
|
@@ -106,6 +118,8 @@ export class AutomineSequencer {
|
|
|
106
118
|
private stopped = false;
|
|
107
119
|
private paused = false;
|
|
108
120
|
private mempoolPoller?: RunningPromise;
|
|
121
|
+
/** Loop that settles epochs and reconciles the clock while {@link AutomineSequencerDeps.autoSettle} is set. */
|
|
122
|
+
private settler?: RunningPromise;
|
|
109
123
|
private publisher?: SequencerPublisher;
|
|
110
124
|
private attestorAddress?: EthAddress;
|
|
111
125
|
|
|
@@ -115,6 +129,9 @@ export class AutomineSequencer {
|
|
|
115
129
|
/** Last L2 slot we published a checkpoint for (-1 means none yet). */
|
|
116
130
|
private lastBuiltSlot: number = -1;
|
|
117
131
|
|
|
132
|
+
/** Lazily-built cheat codes for synthetic epoch settlement (outbox roots + proven tip). */
|
|
133
|
+
private rollupCheatCodes?: RollupCheatCodes;
|
|
134
|
+
|
|
118
135
|
constructor(deps: AutomineSequencerDeps) {
|
|
119
136
|
this.deps = deps;
|
|
120
137
|
this.log = deps.log ?? createLogger('sequencer:automine');
|
|
@@ -122,6 +139,14 @@ export class AutomineSequencer {
|
|
|
122
139
|
this.pollIntervalMs = deps.pollIntervalMs ?? 50;
|
|
123
140
|
}
|
|
124
141
|
|
|
142
|
+
/** Cheat codes for the rollup, built from the same EthCheatCodes the sequencer uses for time control. */
|
|
143
|
+
private getRollupCheatCodes(): RollupCheatCodes {
|
|
144
|
+
this.rollupCheatCodes ??= new RollupCheatCodes(this.deps.ethCheatCodes, {
|
|
145
|
+
rollupAddress: this.deps.config.rollupAddress,
|
|
146
|
+
});
|
|
147
|
+
return this.rollupCheatCodes;
|
|
148
|
+
}
|
|
149
|
+
|
|
125
150
|
/**
|
|
126
151
|
* Starts the sequencer. Switches anvil into automine mode (no interval mining),
|
|
127
152
|
* acquires a publisher, and begins polling the mempool for pending txs.
|
|
@@ -149,6 +174,13 @@ export class AutomineSequencer {
|
|
|
149
174
|
// notification. Tests can also call `buildIfPending()` directly to skip the poll wait.
|
|
150
175
|
this.mempoolPoller = new RunningPromise(() => this.maybeEnqueueBuild(), this.log, this.pollIntervalMs);
|
|
151
176
|
this.mempoolPoller.start();
|
|
177
|
+
|
|
178
|
+
// Local-network only: settle epochs and reconcile the clock to L1 as checkpoints land. Both run
|
|
179
|
+
// through the same serial queue as builds/warps, so they never interleave with a checkpoint build.
|
|
180
|
+
if (this.deps.autoSettle) {
|
|
181
|
+
this.settler = new RunningPromise(() => this.maybeSettle(), this.log, this.deps.settlePollIntervalMs ?? 200);
|
|
182
|
+
this.settler.start();
|
|
183
|
+
}
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
/**
|
|
@@ -163,6 +195,7 @@ export class AutomineSequencer {
|
|
|
163
195
|
this.stopped = true;
|
|
164
196
|
this.running = false;
|
|
165
197
|
await this.mempoolPoller?.stop();
|
|
198
|
+
await this.settler?.stop();
|
|
166
199
|
await this.queue.end();
|
|
167
200
|
await this.deps.stopExtras?.();
|
|
168
201
|
this.log.info('AutomineSequencer stopped');
|
|
@@ -241,14 +274,22 @@ export class AutomineSequencer {
|
|
|
241
274
|
/**
|
|
242
275
|
* Warps L1 timestamp to `targetTimestampSec`. Rounded up to the next aztec-slot
|
|
243
276
|
* boundary so the next build lands on a fresh slot. Atomic with respect to builds —
|
|
244
|
-
* 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}).
|
|
245
279
|
*/
|
|
246
280
|
public warpTo(targetTimestampSec: number): Promise<void> {
|
|
247
281
|
return this.queue.put(() => this.runWarp(targetTimestampSec));
|
|
248
282
|
}
|
|
249
283
|
|
|
250
|
-
/**
|
|
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
|
+
*/
|
|
251
289
|
public warpBy(deltaSec: number): Promise<void> {
|
|
290
|
+
if (deltaSec <= 0) {
|
|
291
|
+
throw new Error(`warpL2TimeAtLeastBy: duration must be positive, got ${deltaSec} seconds.`);
|
|
292
|
+
}
|
|
252
293
|
return this.queue.put(async () => {
|
|
253
294
|
const current = await this.deps.ethCheatCodes.lastBlockTimestamp();
|
|
254
295
|
await this.runWarp(current + deltaSec);
|
|
@@ -271,14 +312,57 @@ export class AutomineSequencer {
|
|
|
271
312
|
return this.queue.syncPoint();
|
|
272
313
|
}
|
|
273
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Synthetically "proves" the L2 chain up to `upToCheckpoint` (default: the latest checkpointed
|
|
317
|
+
* checkpoint). For every epoch newly covered — including a partial final epoch — the epoch out hash
|
|
318
|
+
* is written into the L1 Outbox so the L2-to-L1 messages in those checkpoints become consumable,
|
|
319
|
+
* then the rollup's proven tip is advanced to the target. There is no real proof; this is the
|
|
320
|
+
* local-network equivalent of an epoch proof landing on L1.
|
|
321
|
+
*
|
|
322
|
+
* Clamps the target down to the latest checkpointed checkpoint and no-ops when it is already proven.
|
|
323
|
+
* Runs inside the serial queue so it never interleaves with a build or warp.
|
|
324
|
+
*
|
|
325
|
+
* @returns The proven checkpoint after the call (the target, or the existing proven tip on no-op).
|
|
326
|
+
*/
|
|
327
|
+
public prove(upToCheckpoint?: CheckpointNumber): Promise<CheckpointNumber> {
|
|
328
|
+
return this.queue.put(() => this.runProve(upToCheckpoint));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Auto-settle tick (local-network only). Proving up to the latest checkpointed checkpoint also
|
|
333
|
+
* reconciles the clock at the head of {@link runProve}, so this single tick keeps both the
|
|
334
|
+
* proven tip and the date provider current even when no build is happening.
|
|
335
|
+
*/
|
|
336
|
+
private async maybeSettle(): Promise<void> {
|
|
337
|
+
if (!this.running) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await this.prove();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
this.log.warn('Automine auto-settle tick failed', { err: err instanceof Error ? err.message : String(err) });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Advances the injected date provider to the latest *mined* L1 timestamp when it has fallen behind
|
|
349
|
+
* (e.g. an unrelated L1 tx mined a block between our builds). Never advances to the pending, un-mined
|
|
350
|
+
* timestamp. Keeps node-side consumers of `dateProvider.now()` aligned with L1 without our own builds.
|
|
351
|
+
*/
|
|
352
|
+
private async reconcileDateProvider(): Promise<void> {
|
|
353
|
+
const lastTsMs = (await this.deps.ethCheatCodes.lastBlockTimestamp()) * 1000;
|
|
354
|
+
if (lastTsMs > this.deps.dateProvider.now()) {
|
|
355
|
+
this.deps.dateProvider.setTime(lastTsMs);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
274
359
|
/** Called from the mempool poller. Enqueues a build if there are pending txs. */
|
|
275
360
|
private async maybeEnqueueBuild(): Promise<void> {
|
|
276
361
|
if (!this.running || this.paused || this.buildQueued) {
|
|
277
362
|
return;
|
|
278
363
|
}
|
|
279
364
|
try {
|
|
280
|
-
|
|
281
|
-
if (pending > 0) {
|
|
365
|
+
if (await this.deps.p2pClient.hasEligiblePendingTxs(1)) {
|
|
282
366
|
// Fire-and-forget; the build result is delivered via `buildIfPending()` callers,
|
|
283
367
|
// not via the poller.
|
|
284
368
|
void this.buildIfPending().catch(err => {
|
|
@@ -290,19 +374,27 @@ export class AutomineSequencer {
|
|
|
290
374
|
}
|
|
291
375
|
}
|
|
292
376
|
|
|
293
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* Builds and publishes one checkpoint, returning the built block — or `undefined` when there was
|
|
379
|
+
* nothing to build or the propose failed. A failed propose mines no checkpoint on L1 (it reverts
|
|
380
|
+
* inside the multicall or is never sent), so recovery is purely local: the optimistic archiver insert
|
|
381
|
+
* is rolled back, the block's txs return to the pending pool, and the L1 nonce is reset. No L1 reorg
|
|
382
|
+
* is performed, and the build is not retried inline — once the txs are back in the pending pool the
|
|
383
|
+
* mempool poller re-enqueues a build on its next tick (see {@link maybeEnqueueBuild}).
|
|
384
|
+
*/
|
|
294
385
|
private async runBuild({ allowEmpty }: { allowEmpty: boolean }): Promise<L2Block | undefined> {
|
|
295
386
|
if (!this.running || !this.publisher || !this.attestorAddress) {
|
|
296
387
|
return undefined;
|
|
297
388
|
}
|
|
389
|
+
await this.reconcileDateProvider();
|
|
298
390
|
|
|
299
|
-
|
|
300
|
-
// For mempool-driven builds, wait for at least `minTxsPerBlock` pending txs (or 1 if not set)
|
|
391
|
+
// For mempool-driven builds, wait for at least `minTxsPerBlock` age-eligible txs (or 1 if not set)
|
|
301
392
|
// before building. This mirrors the production sequencer's `waitForMinTxs` behavior, and is
|
|
302
393
|
// required for tests that bundle multiple txs into one block via `setConfig({ minTxsPerBlock })`.
|
|
303
|
-
// Explicit empty-block / warp paths pass `allowEmpty: true
|
|
394
|
+
// Explicit empty-block / warp paths pass `allowEmpty: true`, giving minRequired 0, which
|
|
395
|
+
// hasEligiblePendingTxs treats as always satisfied so the gate is bypassed.
|
|
304
396
|
const minRequired = allowEmpty ? 0 : Math.max(this.deps.config.minTxsPerBlock ?? 1, 1);
|
|
305
|
-
if (
|
|
397
|
+
if (!(await this.deps.p2pClient.hasEligiblePendingTxs(minRequired))) {
|
|
306
398
|
return undefined;
|
|
307
399
|
}
|
|
308
400
|
|
|
@@ -324,7 +416,10 @@ export class AutomineSequencer {
|
|
|
324
416
|
await this.deps.ethCheatCodes.setNextBlockTimestamp(slotBoundaryTs);
|
|
325
417
|
}
|
|
326
418
|
|
|
327
|
-
const tips = await
|
|
419
|
+
const [tips, proposedCheckpoint] = await Promise.all([
|
|
420
|
+
this.deps.l2BlockSource.getL2Tips(),
|
|
421
|
+
this.deps.l2BlockSource.getProposedCheckpointData(),
|
|
422
|
+
]);
|
|
328
423
|
const syncedToBlockNumber = tips.proposed.number;
|
|
329
424
|
|
|
330
425
|
// Ensure world state has processed the archiver's tip before forking. Without this,
|
|
@@ -334,7 +429,8 @@ export class AutomineSequencer {
|
|
|
334
429
|
await this.deps.worldState.syncImmediate(BlockNumber(syncedToBlockNumber));
|
|
335
430
|
|
|
336
431
|
const nextBlockNumber = BlockNumber(syncedToBlockNumber + 1);
|
|
337
|
-
const
|
|
432
|
+
const parentCheckpointNumber = proposedCheckpoint?.checkpointNumber ?? tips.checkpointed.checkpoint.number;
|
|
433
|
+
const checkpointNumber = CheckpointNumber(parentCheckpointNumber + 1);
|
|
338
434
|
const targetEpoch = getEpochAtSlot(SlotNumber(targetSlot), this.deps.l1Constants);
|
|
339
435
|
|
|
340
436
|
this.log.verbose(`Building automine checkpoint`, {
|
|
@@ -342,7 +438,6 @@ export class AutomineSequencer {
|
|
|
342
438
|
blockNumber: nextBlockNumber,
|
|
343
439
|
slot: targetSlot,
|
|
344
440
|
slotTimestamp: slotBoundaryTs,
|
|
345
|
-
txCount,
|
|
346
441
|
allowEmpty,
|
|
347
442
|
});
|
|
348
443
|
|
|
@@ -377,7 +472,8 @@ export class AutomineSequencer {
|
|
|
377
472
|
this.log.getBindings(),
|
|
378
473
|
);
|
|
379
474
|
|
|
380
|
-
|
|
475
|
+
// Block building only executes txs, and automine publishes straight to L1, so the proofs are never needed.
|
|
476
|
+
const pendingTxs = this.deps.p2pClient.iterateEligiblePendingTxs({ includeProof: false });
|
|
381
477
|
|
|
382
478
|
const buildResult = await this.tryBuildBlock(
|
|
383
479
|
checkpointBuilder,
|
|
@@ -419,15 +515,16 @@ export class AutomineSequencer {
|
|
|
419
515
|
// offset — automine is the deliberate non-pipelined exception and builds/publishes in place.
|
|
420
516
|
const result = await this.publisher.sendRequests(SlotNumber(targetSlot));
|
|
421
517
|
|
|
422
|
-
const
|
|
423
|
-
if (!
|
|
424
|
-
this.log.
|
|
518
|
+
const proposeSucceeded = !!result?.successfulActions?.some(action => action === 'propose');
|
|
519
|
+
if (!proposeSucceeded) {
|
|
520
|
+
this.log.warn('Automine propose did not succeed; rolled back optimistic insert, will rebuild from the poller', {
|
|
425
521
|
slot: targetSlot,
|
|
426
522
|
checkpointNumber,
|
|
427
523
|
successful: result?.successfulActions,
|
|
428
524
|
failed: result?.failedActions,
|
|
429
525
|
});
|
|
430
|
-
|
|
526
|
+
await this.rollbackOptimisticInsert(syncedToBlockNumber);
|
|
527
|
+
return undefined;
|
|
431
528
|
}
|
|
432
529
|
|
|
433
530
|
// Force one full L1-sync cycle synchronously. The local addBlock/addProposedCheckpoint
|
|
@@ -439,6 +536,17 @@ export class AutomineSequencer {
|
|
|
439
536
|
const newL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp();
|
|
440
537
|
this.deps.dateProvider.setTime(newL1Ts * 1000);
|
|
441
538
|
|
|
539
|
+
// A successful propose is validated on-chain against the mined L1 block's slot, so the mined slot
|
|
540
|
+
// should equal our target; warn (without rolling back — the checkpoint is on L1) if it ever differs.
|
|
541
|
+
const minedSlot = Number(getSlotAtTimestamp(BigInt(newL1Ts), this.deps.l1Constants));
|
|
542
|
+
if (minedSlot !== targetSlot) {
|
|
543
|
+
this.log.warn(`Automine checkpoint mined in slot ${minedSlot} but targeted ${targetSlot}`, {
|
|
544
|
+
minedSlot,
|
|
545
|
+
targetSlot,
|
|
546
|
+
checkpointNumber,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
442
550
|
this.lastBuiltSlot = targetSlot;
|
|
443
551
|
|
|
444
552
|
this.log.verbose(`Automine checkpoint published`, {
|
|
@@ -452,6 +560,23 @@ export class AutomineSequencer {
|
|
|
452
560
|
return buildResult.block;
|
|
453
561
|
}
|
|
454
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Undoes an optimistic archiver insert (uncheckpointed block + proposed checkpoint) without reorging
|
|
565
|
+
* L1: removes the uncheckpointed block and evicts the proposed checkpoint that referenced it, so the
|
|
566
|
+
* proposed tip drops back to `toBlockNumber`. `p2pClient.sync()` then observes the lowered tip and
|
|
567
|
+
* restores the block's txs to the pending pool; `worldState.syncImmediate()` drops any applied effects;
|
|
568
|
+
* and the L1 nonce is reset (a reverted-but-mined propose consumes one) so the build can be retried.
|
|
569
|
+
*
|
|
570
|
+
* Note: `archiver.rollbackTo` is NOT usable here — it is checkpoint-granular and no-ops on a
|
|
571
|
+
* proposed-only tip (the inserted checkpoint is not yet in the checkpointed set).
|
|
572
|
+
*/
|
|
573
|
+
private async rollbackOptimisticInsert(toBlockNumber: BlockNumber): Promise<void> {
|
|
574
|
+
await this.deps.archiver.removeUncheckpointedBlocksAfter(toBlockNumber);
|
|
575
|
+
await this.deps.p2pClient.sync();
|
|
576
|
+
await this.deps.worldState.syncImmediate();
|
|
577
|
+
this.deps.l1TxUtils.forEach(utils => utils.resetNonce());
|
|
578
|
+
}
|
|
579
|
+
|
|
455
580
|
/**
|
|
456
581
|
* Warps L1 timestamp to (or past) `targetTimestampSec`, rounded up to the next aztec-slot
|
|
457
582
|
* boundary, by queuing an empty-checkpoint build at that slot. Mines exactly one L1 block
|
|
@@ -459,6 +584,7 @@ export class AutomineSequencer {
|
|
|
459
584
|
* the mined block lands on the slot boundary.
|
|
460
585
|
*/
|
|
461
586
|
private async runWarp(targetTimestampSec: number): Promise<void> {
|
|
587
|
+
await this.reconcileDateProvider();
|
|
462
588
|
const currentL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp();
|
|
463
589
|
if (targetTimestampSec <= currentL1Ts) {
|
|
464
590
|
this.log.debug(`Warp target ${targetTimestampSec} is not in the future of current L1 ts ${currentL1Ts}`);
|
|
@@ -536,6 +662,81 @@ export class AutomineSequencer {
|
|
|
536
662
|
});
|
|
537
663
|
}
|
|
538
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Writes outbox roots for every epoch newly covered up to `maybeCheckpoint` and advances the proven
|
|
667
|
+
* tip. Settles each fully-covered epoch in full and the final epoch only up to the target checkpoint
|
|
668
|
+
* (a partial epoch, which the AZIP-14 Outbox supports via per-`numCheckpointsInEpoch` roots).
|
|
669
|
+
*/
|
|
670
|
+
private async runProve(upToCheckpoint?: CheckpointNumber): Promise<CheckpointNumber> {
|
|
671
|
+
await this.reconcileDateProvider();
|
|
672
|
+
const rollupCheatCodes = this.getRollupCheatCodes();
|
|
673
|
+
const tips = await this.deps.l2BlockSource.getL2Tips();
|
|
674
|
+
const checkpointedTip = tips.checkpointed.checkpoint.number;
|
|
675
|
+
|
|
676
|
+
// Never prove beyond what the archiver has actually checkpointed; default to that tip.
|
|
677
|
+
const target = CheckpointNumber(Math.min(upToCheckpoint ?? checkpointedTip, checkpointedTip));
|
|
678
|
+
|
|
679
|
+
const { proven } = await rollupCheatCodes.getTips();
|
|
680
|
+
if (target <= proven) {
|
|
681
|
+
this.log.debug(`Checkpoint ${target} already proven`, { target, proven, checkpointedTip });
|
|
682
|
+
return proven;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const startEpoch = await this.getEpochOfCheckpoint(CheckpointNumber(proven + 1));
|
|
686
|
+
const endEpoch = await this.getEpochOfCheckpoint(target);
|
|
687
|
+
if (startEpoch === undefined || endEpoch === undefined) {
|
|
688
|
+
this.log.warn(`Cannot resolve epoch range to prove up to checkpoint ${target}`, {
|
|
689
|
+
target,
|
|
690
|
+
proven,
|
|
691
|
+
startEpoch,
|
|
692
|
+
endEpoch,
|
|
693
|
+
});
|
|
694
|
+
return proven;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
for (let epoch = startEpoch; epoch <= endEpoch; epoch++) {
|
|
698
|
+
const lastCovered = await settleEpochOutbox({
|
|
699
|
+
rollupCheatCodes,
|
|
700
|
+
l2BlockSource: this.deps.l2BlockSource,
|
|
701
|
+
epoch: EpochNumber(epoch),
|
|
702
|
+
maxCheckpoint: epoch === endEpoch ? target : undefined,
|
|
703
|
+
log: this.log,
|
|
704
|
+
});
|
|
705
|
+
if (lastCovered === undefined) {
|
|
706
|
+
// An epoch in (proven, target] with no checkpointed blocks — expected when warps skip a whole
|
|
707
|
+
// epoch. Logged so it's distinguishable from the archiver failing to serve the epoch's blocks.
|
|
708
|
+
this.log.debug(`No checkpointed blocks to settle for epoch ${epoch} while proving to ${target}`, {
|
|
709
|
+
epoch,
|
|
710
|
+
target,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
await rollupCheatCodes.markAsProven(target);
|
|
716
|
+
// Settlement is a direct L1 storage write that mines no block, unlike a real epoch proof landing
|
|
717
|
+
// on L1. The archiver's L1 sync short-circuits while the L1 block hash is unchanged, so it would
|
|
718
|
+
// never re-read the proven tip until the next build/warp mines a block. Mine one L1 block so the
|
|
719
|
+
// block hash advances, then force an immediate sync that observes the new proven checkpoint. Use
|
|
720
|
+
// evmMine (not mineEmptyBlock): mineEmptyBlock drops+re-adds the mempool non-atomically, which can
|
|
721
|
+
// silently lose a test's concurrently-submitted direct L1 tx. evm_mine just includes whatever is
|
|
722
|
+
// pending, which is fine here — we only need the block hash to advance.
|
|
723
|
+
await this.deps.ethCheatCodes.evmMine();
|
|
724
|
+
await this.deps.archiver.syncImmediate();
|
|
725
|
+
|
|
726
|
+
this.log.verbose(`Proved up to checkpoint ${target}`, { target, proven, startEpoch, endEpoch });
|
|
727
|
+
return target;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Resolves the epoch a checkpoint belongs to from its slot, or undefined if the archiver lacks it. */
|
|
731
|
+
private async getEpochOfCheckpoint(checkpointNumber: CheckpointNumber): Promise<number | undefined> {
|
|
732
|
+
const checkpointData = await this.deps.l2BlockSource.getCheckpointData({ number: checkpointNumber });
|
|
733
|
+
if (!checkpointData) {
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
const slot = SlotNumber(Number(checkpointData.header.slotNumber));
|
|
737
|
+
return Number(getEpochAtSlot(slot, this.deps.l1Constants));
|
|
738
|
+
}
|
|
739
|
+
|
|
539
740
|
/**
|
|
540
741
|
* Wraps `checkpointBuilder.buildBlock` with the failed-tx handling shared by both error
|
|
541
742
|
* and success paths: drops the failed txs from the P2P mempool, and returns `undefined`
|