@aztec/sequencer-client 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d
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 +283 -21
- package/dest/client/sequencer-client.d.ts +20 -5
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +26 -22
- package/dest/config.d.ts +13 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +67 -26
- package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_predictor.js +138 -0
- package/dest/global_variable_builder/fee_provider.d.ts +21 -0
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_provider.js +80 -0
- package/dest/global_variable_builder/global_builder.d.ts +7 -26
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +6 -67
- package/dest/global_variable_builder/index.d.ts +3 -1
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/publisher/config.d.ts +7 -3
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +13 -3
- 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 +3 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
- package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
- package/dest/publisher/sequencer-bundle-simulator.js +198 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +0 -1
- package/dest/publisher/sequencer-publisher.d.ts +65 -70
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +413 -559
- 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 +56 -0
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_factory.js +85 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts +189 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_sequencer.js +696 -0
- 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 +67 -36
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +705 -256
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
- 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 +61 -5
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +10 -11
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +34 -20
- 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 +147 -33
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +543 -180
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- 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 +25 -7
- package/package.json +28 -27
- package/src/client/sequencer-client.ts +37 -27
- package/src/config.ts +79 -26
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +182 -0
- package/src/global_variable_builder/fee_provider.ts +97 -0
- package/src/global_variable_builder/global_builder.ts +11 -91
- package/src/global_variable_builder/index.ts +2 -0
- package/src/publisher/config.ts +30 -7
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-bundle-simulator.ts +254 -0
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +458 -622
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/automine/README.md +60 -0
- package/src/sequencer/automine/automine_factory.ts +152 -0
- package/src/sequencer/automine/automine_sequencer.ts +800 -0
- package/src/sequencer/automine/index.ts +6 -0
- package/src/sequencer/checkpoint_proposal_job.ts +823 -316
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/errors.ts +0 -15
- package/src/sequencer/events.ts +66 -5
- package/src/sequencer/metrics.ts +49 -25
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +607 -206
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -2
- package/src/test/utils.ts +61 -10
- package/dest/sequencer/timetable.d.ts +0 -88
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -222
- package/src/sequencer/README.md +0 -531
- package/src/sequencer/timetable.ts +0 -283
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
RollupContract,
|
|
3
|
+
type SimulationOverridesPlan,
|
|
4
|
+
buildSimulationOverridesStateOverride,
|
|
5
|
+
} from '@aztec/ethereum/contracts';
|
|
3
6
|
import type { ViemPublicClient } from '@aztec/ethereum/types';
|
|
4
|
-
import {
|
|
7
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
8
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
9
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
|
-
import { createLogger } from '@aztec/foundation/log';
|
|
8
|
-
import type { DateProvider } from '@aztec/foundation/timer';
|
|
9
10
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
10
|
-
import { type L1RollupConstants,
|
|
11
|
+
import { type L1RollupConstants, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
11
12
|
import { GasFees } from '@aztec/stdlib/gas';
|
|
12
13
|
import type {
|
|
13
|
-
BuildCheckpointGlobalVariablesOpts,
|
|
14
14
|
CheckpointGlobalVariables,
|
|
15
15
|
GlobalVariableBuilder as GlobalVariableBuilderInterface,
|
|
16
16
|
} from '@aztec/stdlib/tx';
|
|
17
|
-
import { GlobalVariables } from '@aztec/stdlib/tx';
|
|
18
17
|
|
|
19
18
|
/** Configuration for the GlobalVariableBuilder (excludes L1 client config). */
|
|
20
19
|
export type GlobalVariableBuilderConfig = {
|
|
21
|
-
|
|
20
|
+
rollupAddress: EthAddress;
|
|
22
21
|
ethereumSlotDuration: number;
|
|
23
22
|
rollupVersion: bigint;
|
|
24
23
|
} & Pick<L1RollupConstants, 'slotDuration' | 'l1GenesisTime'>;
|
|
@@ -27,12 +26,7 @@ export type GlobalVariableBuilderConfig = {
|
|
|
27
26
|
* Simple global variables builder.
|
|
28
27
|
*/
|
|
29
28
|
export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
|
|
30
|
-
private log = createLogger('sequencer:global_variable_builder');
|
|
31
|
-
private currentMinFees: Promise<GasFees> = Promise.resolve(new GasFees(0, 0));
|
|
32
|
-
private currentL1BlockNumber: bigint | undefined = undefined;
|
|
33
|
-
|
|
34
29
|
private readonly rollupContract: RollupContract;
|
|
35
|
-
private readonly ethereumSlotDuration: number;
|
|
36
30
|
private readonly aztecSlotDuration: number;
|
|
37
31
|
private readonly l1GenesisTime: bigint;
|
|
38
32
|
|
|
@@ -40,79 +34,16 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
|
|
|
40
34
|
private version: Fr;
|
|
41
35
|
|
|
42
36
|
constructor(
|
|
43
|
-
private readonly dateProvider: DateProvider,
|
|
44
37
|
private readonly publicClient: ViemPublicClient,
|
|
45
38
|
config: GlobalVariableBuilderConfig,
|
|
46
39
|
) {
|
|
47
40
|
this.version = new Fr(config.rollupVersion);
|
|
48
41
|
this.chainId = new Fr(this.publicClient.chain!.id);
|
|
49
42
|
|
|
50
|
-
this.ethereumSlotDuration = config.ethereumSlotDuration;
|
|
51
43
|
this.aztecSlotDuration = config.slotDuration;
|
|
52
44
|
this.l1GenesisTime = config.l1GenesisTime;
|
|
53
45
|
|
|
54
|
-
this.rollupContract = new RollupContract(this.publicClient, config.
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Computes the "current" min fees, e.g., the price that you currently should pay to get include in the next block
|
|
59
|
-
* @returns Min fees for the next block
|
|
60
|
-
*/
|
|
61
|
-
private async computeCurrentMinFees(): Promise<GasFees> {
|
|
62
|
-
// Since this might be called in the middle of a slot where a block might have been published,
|
|
63
|
-
// we need to fetch the last block written, and estimate the earliest timestamp for the next block.
|
|
64
|
-
// The timestamp of that last block will act as a lower bound for the next block.
|
|
65
|
-
|
|
66
|
-
const lastCheckpoint = await this.rollupContract.getPendingCheckpoint();
|
|
67
|
-
const earliestTimestamp = await this.rollupContract.getTimestampForSlot(
|
|
68
|
-
SlotNumber.fromBigInt(BigInt(lastCheckpoint.slotNumber) + 1n),
|
|
69
|
-
);
|
|
70
|
-
const nextEthTimestamp = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), {
|
|
71
|
-
l1GenesisTime: this.l1GenesisTime,
|
|
72
|
-
ethereumSlotDuration: this.ethereumSlotDuration,
|
|
73
|
-
});
|
|
74
|
-
const timestamp = earliestTimestamp > nextEthTimestamp ? earliestTimestamp : nextEthTimestamp;
|
|
75
|
-
|
|
76
|
-
return new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
public async getCurrentMinFees(): Promise<GasFees> {
|
|
80
|
-
// Get the current block number
|
|
81
|
-
const blockNumber = await this.publicClient.getBlockNumber();
|
|
82
|
-
|
|
83
|
-
// If the L1 block number has changed then chain a new promise to get the current min fees
|
|
84
|
-
if (this.currentL1BlockNumber === undefined || blockNumber > this.currentL1BlockNumber) {
|
|
85
|
-
this.currentL1BlockNumber = blockNumber;
|
|
86
|
-
this.currentMinFees = this.currentMinFees.then(() => this.computeCurrentMinFees());
|
|
87
|
-
}
|
|
88
|
-
return this.currentMinFees;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Simple builder of global variables.
|
|
93
|
-
* @param blockNumber - The block number to build global variables for.
|
|
94
|
-
* @param coinbase - The address to receive block reward.
|
|
95
|
-
* @param feeRecipient - The address to receive fees.
|
|
96
|
-
* @param slotNumber - The slot number to use for the global variables, if undefined it will be calculated.
|
|
97
|
-
* @returns The global variables for the given block number.
|
|
98
|
-
*/
|
|
99
|
-
public async buildGlobalVariables(
|
|
100
|
-
blockNumber: BlockNumber,
|
|
101
|
-
coinbase: EthAddress,
|
|
102
|
-
feeRecipient: AztecAddress,
|
|
103
|
-
maybeSlot?: SlotNumber,
|
|
104
|
-
): Promise<GlobalVariables> {
|
|
105
|
-
const slot: SlotNumber =
|
|
106
|
-
maybeSlot ??
|
|
107
|
-
(await this.rollupContract.getSlotAt(
|
|
108
|
-
getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), {
|
|
109
|
-
l1GenesisTime: this.l1GenesisTime,
|
|
110
|
-
ethereumSlotDuration: this.ethereumSlotDuration,
|
|
111
|
-
}),
|
|
112
|
-
));
|
|
113
|
-
|
|
114
|
-
const checkpointGlobalVariables = await this.buildCheckpointGlobalVariables(coinbase, feeRecipient, slot);
|
|
115
|
-
return GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables });
|
|
46
|
+
this.rollupContract = new RollupContract(this.publicClient, config.rollupAddress);
|
|
116
47
|
}
|
|
117
48
|
|
|
118
49
|
/** Builds global variables that are constant throughout a checkpoint. */
|
|
@@ -120,7 +51,7 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
|
|
|
120
51
|
coinbase: EthAddress,
|
|
121
52
|
feeRecipient: AztecAddress,
|
|
122
53
|
slotNumber: SlotNumber,
|
|
123
|
-
|
|
54
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
124
55
|
): Promise<CheckpointGlobalVariables> {
|
|
125
56
|
const { chainId, version } = this;
|
|
126
57
|
|
|
@@ -129,18 +60,7 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
|
|
|
129
60
|
l1GenesisTime: this.l1GenesisTime,
|
|
130
61
|
});
|
|
131
62
|
|
|
132
|
-
|
|
133
|
-
// the fee computation matches what L1 will see when the previous pipelined checkpoint has landed.
|
|
134
|
-
const pendingNumberOverride = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
135
|
-
opts?.forcePendingCheckpointNumber,
|
|
136
|
-
);
|
|
137
|
-
const feeHeaderOverride = opts?.forceProposedFeeHeader
|
|
138
|
-
? await this.rollupContract.makeFeeHeaderOverride(
|
|
139
|
-
opts.forceProposedFeeHeader.checkpointNumber,
|
|
140
|
-
opts.forceProposedFeeHeader.feeHeader,
|
|
141
|
-
)
|
|
142
|
-
: [];
|
|
143
|
-
const stateOverride = RollupContract.mergeStateOverrides(pendingNumberOverride, feeHeaderOverride);
|
|
63
|
+
const stateOverride = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
144
64
|
const gasFees = new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true, stateOverride));
|
|
145
65
|
|
|
146
66
|
return { chainId, version, slotNumber, timestamp, coinbase, feeRecipient, gasFees };
|
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. */
|
|
@@ -115,12 +124,13 @@ export function getPublisherConfigFromSequencerConfig(config: SequencerPublisher
|
|
|
115
124
|
};
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
export const proverTxSenderConfigMappings: ConfigMappingsType<
|
|
127
|
+
export const proverTxSenderConfigMappings: ConfigMappingsType<ProverTxSenderConfig> = {
|
|
119
128
|
...l1ReaderConfigMappings,
|
|
120
129
|
proverPublisherPrivateKeys: {
|
|
121
130
|
env: `PROVER_PUBLISHER_PRIVATE_KEYS`,
|
|
122
131
|
description: 'The private keys to be used by the prover publisher.',
|
|
123
|
-
parseEnv: (val: string) =>
|
|
132
|
+
parseEnv: (val: string) =>
|
|
133
|
+
val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}` as `0x${string}`)),
|
|
124
134
|
defaultValue: [],
|
|
125
135
|
fallback: [`PROVER_PUBLISHER_PRIVATE_KEY`],
|
|
126
136
|
},
|
|
@@ -132,12 +142,13 @@ export const proverTxSenderConfigMappings: ConfigMappingsType<Omit<ProverTxSende
|
|
|
132
142
|
},
|
|
133
143
|
};
|
|
134
144
|
|
|
135
|
-
export const sequencerTxSenderConfigMappings: ConfigMappingsType<
|
|
145
|
+
export const sequencerTxSenderConfigMappings: ConfigMappingsType<SequencerTxSenderConfig> = {
|
|
136
146
|
...l1ReaderConfigMappings,
|
|
137
147
|
sequencerPublisherPrivateKeys: {
|
|
138
148
|
env: `SEQ_PUBLISHER_PRIVATE_KEYS`,
|
|
139
149
|
description: 'The private keys to be used by the sequencer publisher.',
|
|
140
|
-
parseEnv: (val: string) =>
|
|
150
|
+
parseEnv: (val: string) =>
|
|
151
|
+
val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}` as `0x${string}`)),
|
|
141
152
|
defaultValue: [],
|
|
142
153
|
fallback: [`SEQ_PUBLISHER_PRIVATE_KEY`],
|
|
143
154
|
},
|
|
@@ -166,7 +177,19 @@ export const sequencerPublisherConfigMappings: ConfigMappingsType<SequencerPubli
|
|
|
166
177
|
sequencerPublisherForwarderAddress: {
|
|
167
178
|
env: `SEQ_PUBLISHER_FORWARDER_ADDRESS`,
|
|
168
179
|
description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
|
|
169
|
-
parseEnv: (val: string) =>
|
|
180
|
+
parseEnv: (val: string) => EthAddress.fromString(val),
|
|
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),
|
|
170
193
|
},
|
|
171
194
|
l1TxFailedStore: {
|
|
172
195
|
env: 'L1_TX_FAILED_STORE',
|
|
@@ -192,7 +215,7 @@ export const proverPublisherConfigMappings: ConfigMappingsType<ProverPublisherCo
|
|
|
192
215
|
proverPublisherForwarderAddress: {
|
|
193
216
|
env: `PROVER_PUBLISHER_FORWARDER_ADDRESS`,
|
|
194
217
|
description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
|
|
195
|
-
parseEnv: (val: string) =>
|
|
218
|
+
parseEnv: (val: string) => EthAddress.fromString(val),
|
|
196
219
|
},
|
|
197
220
|
...publisherFundingConfigMappings,
|
|
198
221
|
};
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { Branded } from '@aztec/foundation/branded-types';
|
|
2
|
+
|
|
1
3
|
import type { Hex } from 'viem';
|
|
2
4
|
|
|
3
5
|
/** URI pointing to a stored failed L1 transaction. */
|
|
4
|
-
export type FailedL1TxUri = string
|
|
6
|
+
export type FailedL1TxUri = Branded<string, 'FailedL1TxUri'>;
|
|
5
7
|
|
|
6
8
|
/** A failed L1 transaction captured for debugging and replay. */
|
|
7
9
|
export type FailedL1Tx = {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { Multicall3, type RollupContract, buildSimulationOverridesStateOverride } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { type L1TxUtils, MAX_L1_TX_LIMIT } from '@aztec/ethereum/l1-tx-utils';
|
|
4
|
+
import { formatViemError } from '@aztec/ethereum/utils';
|
|
5
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
6
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
7
|
+
import { getLastL1SlotTimestampForL2Slot } from '@aztec/stdlib/epoch-helpers';
|
|
8
|
+
|
|
9
|
+
import type { Hex, StateOverride } from 'viem';
|
|
10
|
+
|
|
11
|
+
import type { RequestWithExpiry } from './sequencer-publisher.js';
|
|
12
|
+
|
|
13
|
+
/** A request that was dropped by bundle simulation, with the decoded revert reason. */
|
|
14
|
+
export type DroppedRequest = {
|
|
15
|
+
request: RequestWithExpiry;
|
|
16
|
+
revertReason: string | undefined;
|
|
17
|
+
returnData: Hex | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of {@link SequencerBundleSimulator.simulate}.
|
|
22
|
+
*
|
|
23
|
+
* - `success`: simulation succeeded. `requests` is the filtered survivor list, `gasLimit` is
|
|
24
|
+
* the bumped gas limit derived from `gasUsed` (plus blob evaluation gas). `droppedRequests`
|
|
25
|
+
* lists the entries that were observed to revert in simulation.
|
|
26
|
+
* - `fallback`: the node does not support eth_simulateV1 (or the simulate call threw). The
|
|
27
|
+
* caller should send `requests` as-is with a safe gas limit (e.g. {@link MAX_L1_TX_LIMIT}).
|
|
28
|
+
* `droppedRequests` carries any entries that the first pass already proved reverted, so the
|
|
29
|
+
* caller does not re-include them when the second pass falls back.
|
|
30
|
+
* - `aborted`: the bundle cannot be sent. `droppedRequests` contains only entries that were
|
|
31
|
+
* actually observed to revert (so they can be reported as simulation failures); it is empty
|
|
32
|
+
* when the abort was caused by an empty input bundle.
|
|
33
|
+
*/
|
|
34
|
+
export type BundleSimulateResult =
|
|
35
|
+
| { kind: 'success'; requests: RequestWithExpiry[]; gasLimit: bigint; droppedRequests: DroppedRequest[] }
|
|
36
|
+
| { kind: 'fallback'; requests: RequestWithExpiry[]; droppedRequests: DroppedRequest[] }
|
|
37
|
+
| { kind: 'aborted'; reason: AbortReason; droppedRequests: DroppedRequest[] };
|
|
38
|
+
|
|
39
|
+
export type AbortReason = 'empty-bundle' | 'all-reverted' | 'second-pass-reverts';
|
|
40
|
+
|
|
41
|
+
type SimulatePassResult =
|
|
42
|
+
| { kind: 'decoded'; survivors: RequestWithExpiry[]; droppedRequests: DroppedRequest[]; gasUsed: bigint }
|
|
43
|
+
| { kind: 'fallback' };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Bundle-level simulator for the aggregate3 payload that `SequencerPublisher` is about to send.
|
|
47
|
+
*
|
|
48
|
+
* Runs `eth_simulateV1` against `Multicall3.aggregate3`, drops entries that revert, and returns
|
|
49
|
+
* a gasLimit for the survivors. When `eth_simulateV1` is unavailable, signals fallback to the
|
|
50
|
+
* caller so it can send the bundle as-is with a conservative gas limit.
|
|
51
|
+
*/
|
|
52
|
+
export class SequencerBundleSimulator {
|
|
53
|
+
private readonly log: Logger;
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly deps: {
|
|
57
|
+
getL1TxUtils: () => L1TxUtils;
|
|
58
|
+
rollupContract: RollupContract;
|
|
59
|
+
epochCache: EpochCache;
|
|
60
|
+
log?: Logger;
|
|
61
|
+
},
|
|
62
|
+
) {
|
|
63
|
+
this.log = deps.log ?? createLogger('sequencer:publisher:bundle-simulator');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Simulates the given bundle near the end of the target slot and filters out entries
|
|
68
|
+
* that revert.
|
|
69
|
+
*
|
|
70
|
+
* - If all entries pass on the first pass, returns `success` with the gasLimit.
|
|
71
|
+
* - If some entries revert, re-simulates the survivors. If the second pass is clean, returns
|
|
72
|
+
* `success` with the survivors and dropped entries. If the second pass surfaces any revert,
|
|
73
|
+
* returns `aborted` — we refuse to send a bundle whose composition still has internal
|
|
74
|
+
* reverts after one round of filtering.
|
|
75
|
+
* - If eth_simulateV1 is unavailable, returns `fallback`. The caller is expected to send the
|
|
76
|
+
* bundle as-is with a safe gas limit.
|
|
77
|
+
*
|
|
78
|
+
* The simulation `block.timestamp` is the last L1 slot timestamp inside the target L2 slot.
|
|
79
|
+
* This still maps to the target L2 slot for propose's `validateHeader` and EIP-712 signature
|
|
80
|
+
* checks, while avoiding eth_simulateV1 rejecting a child block whose timestamp is not strictly
|
|
81
|
+
* greater than the current L1 head.
|
|
82
|
+
*
|
|
83
|
+
* Known limitation: on networks where L1 is mining behind cadence (missed L1 slots, anvil with
|
|
84
|
+
* overridden timestamps), the actual `block.timestamp` at send time can land in the prior L2
|
|
85
|
+
* slot. In that case `propose` would revert silently inside the multicall. The simulator does
|
|
86
|
+
* not detect this case because it simulates inside the target slot — the prior implementation
|
|
87
|
+
* used `min(predictedNextL1Ts, targetTimestamp)` to surface this failure mode at simulate time.
|
|
88
|
+
*/
|
|
89
|
+
public async simulate(validRequests: RequestWithExpiry[], targetSlot: SlotNumber): Promise<BundleSimulateResult> {
|
|
90
|
+
if (validRequests.length === 0) {
|
|
91
|
+
return { kind: 'aborted', reason: 'empty-bundle', droppedRequests: [] };
|
|
92
|
+
}
|
|
93
|
+
// Pin the publisher we'll use across the whole simulate call so that the publisher's rotation
|
|
94
|
+
// can't change l1TxUtils mid-flight.
|
|
95
|
+
const l1TxUtils = this.deps.getL1TxUtils();
|
|
96
|
+
|
|
97
|
+
const proposeRequest = validRequests.find(r => r.action === 'propose');
|
|
98
|
+
const simulateTimestamp = getLastL1SlotTimestampForL2Slot(targetSlot, this.deps.epochCache.getL1Constants());
|
|
99
|
+
const firstPassOverrides = await this.buildStateOverrides(!!proposeRequest);
|
|
100
|
+
|
|
101
|
+
const firstPass = await this.simulateAndDecode(l1TxUtils, validRequests, simulateTimestamp, firstPassOverrides);
|
|
102
|
+
|
|
103
|
+
if (firstPass.kind === 'fallback') {
|
|
104
|
+
this.log.warn('Bundle simulate fallback (eth_simulateV1 unavailable); caller will send bundle as-is', {
|
|
105
|
+
actions: validRequests.map(r => r.action),
|
|
106
|
+
});
|
|
107
|
+
return { kind: 'fallback', requests: validRequests, droppedRequests: [] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (firstPass.survivors.length === 0) {
|
|
111
|
+
this.log.warn('All bundle entries dropped in simulation; aborting send', {
|
|
112
|
+
actions: validRequests.map(r => r.action),
|
|
113
|
+
});
|
|
114
|
+
return { kind: 'aborted', reason: 'all-reverted', droppedRequests: firstPass.droppedRequests };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (firstPass.droppedRequests.length === 0) {
|
|
118
|
+
return this.buildSuccessResult(l1TxUtils, firstPass.survivors, [], firstPass.gasUsed, proposeRequest);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.log.warn('Some bundle entries reverted; re-simulating reduced bundle', {
|
|
122
|
+
droppedActions: firstPass.droppedRequests.map(d => d.request.action),
|
|
123
|
+
remainingActions: firstPass.survivors.map(r => r.action),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Rebuild overrides for the reduced bundle: if propose was dropped, we no longer need the blob-check override
|
|
127
|
+
const proposeSurvived = proposeRequest !== undefined && firstPass.survivors.includes(proposeRequest);
|
|
128
|
+
const secondPassOverrides = proposeSurvived ? firstPassOverrides : await this.buildStateOverrides(false);
|
|
129
|
+
const secondPass = await this.simulateAndDecode(
|
|
130
|
+
l1TxUtils,
|
|
131
|
+
firstPass.survivors,
|
|
132
|
+
simulateTimestamp,
|
|
133
|
+
secondPassOverrides,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (secondPass.kind === 'fallback') {
|
|
137
|
+
this.log.warn(
|
|
138
|
+
'Bundle simulate errored on second pass (eth_simulateV1 unavailable); sending first-pass survivors as-is',
|
|
139
|
+
{
|
|
140
|
+
actions: firstPass.survivors.map(r => r.action),
|
|
141
|
+
droppedActions: firstPass.droppedRequests.map(d => d.request.action),
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
return { kind: 'fallback', requests: firstPass.survivors, droppedRequests: firstPass.droppedRequests };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// We refuse to chase reverts through repeated trimming: anything other than a clean second pass aborts the whole send
|
|
148
|
+
if (secondPass.droppedRequests.length > 0) {
|
|
149
|
+
this.log.error('Re-simulate surfaced reverts; aborting send', {
|
|
150
|
+
secondPassDroppedActions: secondPass.droppedRequests.map(d => d.request.action),
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
kind: 'aborted',
|
|
154
|
+
reason: 'second-pass-reverts',
|
|
155
|
+
droppedRequests: [...firstPass.droppedRequests, ...secondPass.droppedRequests],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return this.buildSuccessResult(
|
|
160
|
+
l1TxUtils,
|
|
161
|
+
secondPass.survivors,
|
|
162
|
+
firstPass.droppedRequests,
|
|
163
|
+
secondPass.gasUsed,
|
|
164
|
+
proposeRequest,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private buildSuccessResult(
|
|
169
|
+
l1TxUtils: L1TxUtils,
|
|
170
|
+
survivors: RequestWithExpiry[],
|
|
171
|
+
droppedRequests: DroppedRequest[],
|
|
172
|
+
bundleGasUsed: bigint,
|
|
173
|
+
proposeRequest: RequestWithExpiry | undefined,
|
|
174
|
+
): BundleSimulateResult {
|
|
175
|
+
const proposeSurvived = proposeRequest !== undefined && survivors.includes(proposeRequest);
|
|
176
|
+
const blobEvaluationGas = proposeSurvived ? (proposeRequest?.blobEvaluationGas ?? 0n) : 0n;
|
|
177
|
+
const gasLimit = this.computeGasLimit(l1TxUtils, bundleGasUsed, blobEvaluationGas);
|
|
178
|
+
this.log.debug('Bundle simulate complete', {
|
|
179
|
+
survivingRequests: survivors.length,
|
|
180
|
+
bundleGasUsed,
|
|
181
|
+
gasLimit,
|
|
182
|
+
actions: survivors.map(r => r.action),
|
|
183
|
+
});
|
|
184
|
+
return { kind: 'success', requests: survivors, gasLimit, droppedRequests };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `gasLimit = bumpGasLimit(ceil(gasUsed * 64 / 63))`, plus blob evaluation gas if a propose
|
|
189
|
+
* survived, capped at the L1 block gas limit.
|
|
190
|
+
*/
|
|
191
|
+
private computeGasLimit(l1TxUtils: L1TxUtils, bundleGasUsed: bigint, blobEvaluationGas: bigint): bigint {
|
|
192
|
+
const gasUsedWithEip150 = (bundleGasUsed * 64n + 62n) / 63n;
|
|
193
|
+
const gasLimit = l1TxUtils.bumpGasLimit(gasUsedWithEip150) + blobEvaluationGas;
|
|
194
|
+
return gasLimit > MAX_L1_TX_LIMIT ? MAX_L1_TX_LIMIT : gasLimit;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* eth_simulateV1 cannot carry blob sidecar data, so disable the rollup's on-chain blob check
|
|
199
|
+
* when a propose is in the bundle.
|
|
200
|
+
*/
|
|
201
|
+
private buildStateOverrides(hasProposeAction: boolean): Promise<StateOverride> {
|
|
202
|
+
return buildSimulationOverridesStateOverride(
|
|
203
|
+
this.deps.rollupContract,
|
|
204
|
+
hasProposeAction ? { disableBlobCheck: true } : undefined,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async simulateAndDecode(
|
|
209
|
+
l1TxUtils: L1TxUtils,
|
|
210
|
+
requests: RequestWithExpiry[],
|
|
211
|
+
simulateTimestamp: bigint,
|
|
212
|
+
stateOverrides: StateOverride,
|
|
213
|
+
): Promise<SimulatePassResult> {
|
|
214
|
+
let simResult: Awaited<ReturnType<typeof Multicall3.simulateAggregate3>>;
|
|
215
|
+
try {
|
|
216
|
+
simResult = await Multicall3.simulateAggregate3(
|
|
217
|
+
requests.map(r => ({ to: r.request.to! as Hex, data: r.request.data! as Hex, abi: r.request.abi })),
|
|
218
|
+
l1TxUtils,
|
|
219
|
+
{
|
|
220
|
+
blockOverrides: { time: simulateTimestamp, gasLimit: MAX_L1_TX_LIMIT * 2n },
|
|
221
|
+
stateOverrides,
|
|
222
|
+
gas: MAX_L1_TX_LIMIT,
|
|
223
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
this.log.warn('Bundle simulate threw; treating as fallback', {
|
|
228
|
+
err: formatViemError(err),
|
|
229
|
+
actions: requests.map(r => r.action),
|
|
230
|
+
});
|
|
231
|
+
return { kind: 'fallback' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (simResult.kind === 'fallback') {
|
|
235
|
+
return { kind: 'fallback' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const survivors: RequestWithExpiry[] = [];
|
|
239
|
+
const droppedRequests: DroppedRequest[] = [];
|
|
240
|
+
for (let i = 0; i < requests.length; i++) {
|
|
241
|
+
const entry = simResult.entries[i];
|
|
242
|
+
if (entry.success) {
|
|
243
|
+
survivors.push(requests[i]);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
droppedRequests.push({
|
|
247
|
+
request: requests[i],
|
|
248
|
+
revertReason: entry.revertReason,
|
|
249
|
+
returnData: entry.returnData,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return { kind: 'decoded', survivors, droppedRequests, gasUsed: simResult.gasUsed };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -7,7 +7,6 @@ import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
|
|
|
7
7
|
import type { PublisherFilter, PublisherManager } from '@aztec/ethereum/publisher-manager';
|
|
8
8
|
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
9
9
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
10
|
-
import type { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
11
10
|
import type { TelemetryClient } from '@aztec/telemetry-client';
|
|
12
11
|
import { NodeKeystoreAdapter } from '@aztec/validator-client';
|
|
13
12
|
|
|
@@ -40,7 +39,6 @@ export class SequencerPublisherFactory {
|
|
|
40
39
|
epochCache: EpochCache;
|
|
41
40
|
rollupContract: RollupContract;
|
|
42
41
|
governanceProposerContract: GovernanceProposerContract;
|
|
43
|
-
slashFactoryContract: SlashFactoryContract;
|
|
44
42
|
nodeKeyStore: NodeKeystoreAdapter;
|
|
45
43
|
logger?: Logger;
|
|
46
44
|
},
|
|
@@ -104,7 +102,6 @@ export class SequencerPublisherFactory {
|
|
|
104
102
|
epochCache: this.deps.epochCache,
|
|
105
103
|
governanceProposerContract: this.deps.governanceProposerContract,
|
|
106
104
|
slashingProposerContract,
|
|
107
|
-
slashFactoryContract: this.deps.slashFactoryContract,
|
|
108
105
|
dateProvider: this.deps.dateProvider,
|
|
109
106
|
metrics: this.publisherMetrics,
|
|
110
107
|
lastActions: this.lastActions,
|