@aztec/sequencer-client 0.49.2 → 0.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +6 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +13 -7
- package/dest/global_variable_builder/global_builder.d.ts +6 -54
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +24 -12
- package/dest/global_variable_builder/index.d.ts +0 -9
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -12
- package/dest/publisher/index.d.ts +0 -8
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -10
- package/dest/publisher/l1-publisher.d.ts +26 -70
- package/dest/publisher/l1-publisher.d.ts.map +1 -1
- package/dest/publisher/l1-publisher.js +168 -19
- package/dest/sequencer/sequencer.d.ts +22 -8
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +76 -14
- package/package.json +18 -17
- package/src/client/sequencer-client.ts +13 -4
- package/src/global_variable_builder/global_builder.ts +38 -62
- package/src/global_variable_builder/index.ts +0 -15
- package/src/publisher/index.ts +0 -14
- package/src/publisher/l1-publisher.ts +217 -95
- package/src/sequencer/sequencer.ts +97 -11
- package/dest/global_variable_builder/viem-reader.d.ts +0 -17
- package/dest/global_variable_builder/viem-reader.d.ts.map +0 -1
- package/dest/global_variable_builder/viem-reader.js +0 -40
- package/dest/publisher/viem-tx-sender.d.ts +0 -59
- package/dest/publisher/viem-tx-sender.d.ts.map +0 -1
- package/dest/publisher/viem-tx-sender.js +0 -236
- package/dest/receiver.d.ts +0 -10
- package/dest/receiver.d.ts.map +0 -1
- package/dest/receiver.js +0 -2
- package/src/global_variable_builder/viem-reader.ts +0 -64
- package/src/publisher/viem-tx-sender.ts +0 -296
- package/src/receiver.ts +0 -11
|
@@ -1,17 +1,34 @@
|
|
|
1
|
-
import { type L2Block } from '@aztec/circuit-types';
|
|
1
|
+
import { type L2Block, type Signature } from '@aztec/circuit-types';
|
|
2
2
|
import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats';
|
|
3
|
-
import {
|
|
3
|
+
import { ETHEREUM_SLOT_DURATION, EthAddress, type Header, type Proof } from '@aztec/circuits.js';
|
|
4
|
+
import { createEthereumChain } from '@aztec/ethereum';
|
|
4
5
|
import { type Fr } from '@aztec/foundation/fields';
|
|
5
6
|
import { createDebugLogger } from '@aztec/foundation/log';
|
|
6
7
|
import { serializeToBuffer } from '@aztec/foundation/serialize';
|
|
7
8
|
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
8
9
|
import { Timer } from '@aztec/foundation/timer';
|
|
10
|
+
import { AvailabilityOracleAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
9
11
|
import { type TelemetryClient } from '@aztec/telemetry-client';
|
|
10
12
|
|
|
11
13
|
import pick from 'lodash.pick';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
import {
|
|
15
|
+
type GetContractReturnType,
|
|
16
|
+
type Hex,
|
|
17
|
+
type HttpTransport,
|
|
18
|
+
type PrivateKeyAccount,
|
|
19
|
+
type PublicClient,
|
|
20
|
+
type WalletClient,
|
|
21
|
+
createPublicClient,
|
|
22
|
+
createWalletClient,
|
|
23
|
+
getAddress,
|
|
24
|
+
getContract,
|
|
25
|
+
hexToBytes,
|
|
26
|
+
http,
|
|
27
|
+
} from 'viem';
|
|
28
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
29
|
+
import type * as chains from 'viem/chains';
|
|
30
|
+
|
|
31
|
+
import { type PublisherConfig, type TxSenderConfig } from './config.js';
|
|
15
32
|
import { L1PublisherMetrics } from './l1-publisher-metrics.js';
|
|
16
33
|
|
|
17
34
|
/**
|
|
@@ -27,7 +44,7 @@ export type TransactionStats = {
|
|
|
27
44
|
};
|
|
28
45
|
|
|
29
46
|
/**
|
|
30
|
-
* Minimal information from a tx receipt
|
|
47
|
+
* Minimal information from a tx receipt.
|
|
31
48
|
*/
|
|
32
49
|
export type MinimalTransactionReceipt = {
|
|
33
50
|
/** True if the tx was successful, false if reverted. */
|
|
@@ -49,84 +66,18 @@ export type MinimalTransactionReceipt = {
|
|
|
49
66
|
*/
|
|
50
67
|
export type Attestation = { isEmpty: boolean; v: number; r: `0x${string}`; s: `0x${string}` };
|
|
51
68
|
|
|
52
|
-
/**
|
|
53
|
-
* Pushes txs to the L1 chain and waits for their completion.
|
|
54
|
-
*/
|
|
55
|
-
export interface L1PublisherTxSender {
|
|
56
|
-
/** Attests to the given archive root. */
|
|
57
|
-
attest(archive: `0x${string}`): Promise<Attestation>;
|
|
58
|
-
|
|
59
|
-
/** Returns the EOA used for sending txs to L1. */
|
|
60
|
-
getSenderAddress(): Promise<EthAddress>;
|
|
61
|
-
|
|
62
|
-
/** Returns the address of the L2 proposer at the NEXT Ethereum block zero if anyone can submit. */
|
|
63
|
-
getProposerAtNextEthBlock(): Promise<EthAddress>;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Publishes tx effects to Availability Oracle.
|
|
67
|
-
* @param encodedBody - Encoded block body.
|
|
68
|
-
* @returns The hash of the mined tx.
|
|
69
|
-
*/
|
|
70
|
-
sendPublishTx(encodedBody: Buffer): Promise<string | undefined>;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Sends a tx to the L1 rollup contract with a new L2 block. Returns once the tx has been mined.
|
|
74
|
-
* @param encodedData - Serialized data for processing the new L2 block.
|
|
75
|
-
* @returns The hash of the mined tx.
|
|
76
|
-
*/
|
|
77
|
-
sendProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined>;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Publishes tx effects to availability oracle and send L2 block to rollup contract
|
|
81
|
-
* @param encodedData - Data for processing the new L2 block.
|
|
82
|
-
* @returns The hash of the tx.
|
|
83
|
-
*/
|
|
84
|
-
sendPublishAndProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined>;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Sends a tx to the L1 rollup contract with a proof. Returns once the tx has been mined.
|
|
88
|
-
* @param encodedData - Serialized data for processing the new L2 block.
|
|
89
|
-
* @returns The hash of the mined tx.
|
|
90
|
-
*/
|
|
91
|
-
sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise<string | undefined>;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Returns a tx receipt if the tx has been mined.
|
|
95
|
-
* @param txHash - Hash of the tx to look for.
|
|
96
|
-
* @returns Undefined if the tx hasn't been mined yet, the receipt otherwise.
|
|
97
|
-
*/
|
|
98
|
-
getTransactionReceipt(txHash: string): Promise<MinimalTransactionReceipt | undefined>;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Returns info on a tx by calling eth_getTransaction.
|
|
102
|
-
* @param txHash - Hash of the tx to look for.
|
|
103
|
-
*/
|
|
104
|
-
getTransactionStats(txHash: string): Promise<TransactionStats | undefined>;
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Returns the current archive root.
|
|
108
|
-
* @returns The current archive root of the rollup contract.
|
|
109
|
-
*/
|
|
110
|
-
getCurrentArchive(): Promise<Buffer>;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Checks if the transaction effects of the given block are available.
|
|
114
|
-
* @param block - The block of which to check whether txs are available.
|
|
115
|
-
* @returns True if the txs are available, false otherwise.
|
|
116
|
-
*/
|
|
117
|
-
checkIfTxsAreAvailable(block: L2Block): Promise<boolean>;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
69
|
/** Arguments to the process method of the rollup contract */
|
|
121
70
|
export type L1ProcessArgs = {
|
|
122
71
|
/** The L2 block header. */
|
|
123
72
|
header: Buffer;
|
|
124
73
|
/** A root of the archive tree after the L2 block is applied. */
|
|
125
74
|
archive: Buffer;
|
|
75
|
+
/** The L2 block's leaf in the archive tree. */
|
|
76
|
+
blockHash: Buffer;
|
|
126
77
|
/** L2 block body. */
|
|
127
78
|
body: Buffer;
|
|
128
79
|
/** Attestations */
|
|
129
|
-
attestations?:
|
|
80
|
+
attestations?: Signature[];
|
|
130
81
|
};
|
|
131
82
|
|
|
132
83
|
/** Arguments to the submitProof method of the rollup contract */
|
|
@@ -151,38 +102,107 @@ export type L1SubmitProofArgs = {
|
|
|
151
102
|
*
|
|
152
103
|
* Adapted from https://github.com/AztecProtocol/aztec2-internal/blob/master/falafel/src/rollup_publisher.ts.
|
|
153
104
|
*/
|
|
154
|
-
export class L1Publisher
|
|
105
|
+
export class L1Publisher {
|
|
155
106
|
private interruptibleSleep = new InterruptibleSleep();
|
|
156
107
|
private sleepTimeMs: number;
|
|
157
108
|
private interrupted = false;
|
|
158
109
|
private metrics: L1PublisherMetrics;
|
|
159
110
|
private log = createDebugLogger('aztec:sequencer:publisher');
|
|
160
111
|
|
|
161
|
-
|
|
112
|
+
private availabilityOracleContract: GetContractReturnType<
|
|
113
|
+
typeof AvailabilityOracleAbi,
|
|
114
|
+
WalletClient<HttpTransport, chains.Chain, PrivateKeyAccount>
|
|
115
|
+
>;
|
|
116
|
+
private rollupContract: GetContractReturnType<
|
|
117
|
+
typeof RollupAbi,
|
|
118
|
+
WalletClient<HttpTransport, chains.Chain, PrivateKeyAccount>
|
|
119
|
+
>;
|
|
120
|
+
private publicClient: PublicClient<HttpTransport, chains.Chain>;
|
|
121
|
+
private account: PrivateKeyAccount;
|
|
122
|
+
|
|
123
|
+
constructor(config: TxSenderConfig & PublisherConfig, client: TelemetryClient) {
|
|
162
124
|
this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000;
|
|
163
125
|
this.metrics = new L1PublisherMetrics(client, 'L1Publisher');
|
|
126
|
+
|
|
127
|
+
const { l1RpcUrl: rpcUrl, l1ChainId: chainId, publisherPrivateKey, l1Contracts } = config;
|
|
128
|
+
const chain = createEthereumChain(rpcUrl, chainId);
|
|
129
|
+
this.account = privateKeyToAccount(publisherPrivateKey);
|
|
130
|
+
const walletClient = createWalletClient({
|
|
131
|
+
account: this.account,
|
|
132
|
+
chain: chain.chainInfo,
|
|
133
|
+
transport: http(chain.rpcUrl),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.publicClient = createPublicClient({
|
|
137
|
+
chain: chain.chainInfo,
|
|
138
|
+
transport: http(chain.rpcUrl),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.availabilityOracleContract = getContract({
|
|
142
|
+
address: getAddress(l1Contracts.availabilityOracleAddress.toString()),
|
|
143
|
+
abi: AvailabilityOracleAbi,
|
|
144
|
+
client: walletClient,
|
|
145
|
+
});
|
|
146
|
+
this.rollupContract = getContract({
|
|
147
|
+
address: getAddress(l1Contracts.rollupAddress.toString()),
|
|
148
|
+
abi: RollupAbi,
|
|
149
|
+
client: walletClient,
|
|
150
|
+
});
|
|
164
151
|
}
|
|
165
152
|
|
|
166
|
-
public
|
|
167
|
-
return
|
|
153
|
+
public getSenderAddress(): Promise<EthAddress> {
|
|
154
|
+
return Promise.resolve(EthAddress.fromString(this.account.address));
|
|
168
155
|
}
|
|
169
156
|
|
|
170
|
-
|
|
171
|
-
|
|
157
|
+
// Computes who will be the L2 proposer at the next Ethereum block
|
|
158
|
+
// Using next Ethereum block so we do NOT need to wait for it being mined before seeing the effect
|
|
159
|
+
// @note Assumes that all ethereum slots have blocks
|
|
160
|
+
async getProposerAtNextEthBlock(): Promise<EthAddress> {
|
|
161
|
+
try {
|
|
162
|
+
const ts = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(ETHEREUM_SLOT_DURATION));
|
|
163
|
+
const submitter = await this.rollupContract.read.getProposerAt([ts]);
|
|
164
|
+
return EthAddress.fromString(submitter);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
this.log.warn(`Failed to get submitter: ${err}`);
|
|
167
|
+
return EthAddress.ZERO;
|
|
168
|
+
}
|
|
172
169
|
}
|
|
173
170
|
|
|
174
171
|
public async isItMyTurnToSubmit(): Promise<boolean> {
|
|
175
|
-
const submitter = await this.
|
|
176
|
-
const sender = await this.
|
|
172
|
+
const submitter = await this.getProposerAtNextEthBlock();
|
|
173
|
+
const sender = await this.getSenderAddress();
|
|
177
174
|
return submitter.isZero() || submitter.equals(sender);
|
|
178
175
|
}
|
|
179
176
|
|
|
177
|
+
public async getCurrentEpochCommittee(): Promise<EthAddress[]> {
|
|
178
|
+
const committee = await this.rollupContract.read.getCurrentEpochCommittee();
|
|
179
|
+
return committee.map(EthAddress.fromString);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
checkIfTxsAreAvailable(block: L2Block): Promise<boolean> {
|
|
183
|
+
const args = [`0x${block.body.getTxsEffectsHash().toString('hex').padStart(64, '0')}`] as const;
|
|
184
|
+
return this.availabilityOracleContract.read.isAvailable(args);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getTransactionStats(txHash: string): Promise<TransactionStats | undefined> {
|
|
188
|
+
const tx = await this.publicClient.getTransaction({ hash: txHash as Hex });
|
|
189
|
+
if (!tx) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const calldata = hexToBytes(tx.input);
|
|
193
|
+
return {
|
|
194
|
+
transactionHash: tx.hash,
|
|
195
|
+
calldataSize: calldata.length,
|
|
196
|
+
calldataGas: getCalldataGasUsage(calldata),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
/**
|
|
181
201
|
* Publishes L2 block on L1.
|
|
182
202
|
* @param block - L2 block to publish.
|
|
183
203
|
* @returns True once the tx has been confirmed and is successful, false on revert or interrupt, blocks otherwise.
|
|
184
204
|
*/
|
|
185
|
-
public async processL2Block(block: L2Block, attestations?:
|
|
205
|
+
public async processL2Block(block: L2Block, attestations?: Signature[]): Promise<boolean> {
|
|
186
206
|
const ctx = { blockNumber: block.number, blockHash: block.hash().toString() };
|
|
187
207
|
// TODO(#4148) Remove this block number check, it's here because we don't currently have proper genesis state on the contract
|
|
188
208
|
const lastArchive = block.header.lastArchive.root.toBuffer();
|
|
@@ -195,6 +215,7 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
195
215
|
const processTxArgs = {
|
|
196
216
|
header: block.header.toBuffer(),
|
|
197
217
|
archive: block.archive.root.toBuffer(),
|
|
218
|
+
blockHash: block.header.hash().toBuffer(),
|
|
198
219
|
body: encodedBody,
|
|
199
220
|
attestations,
|
|
200
221
|
};
|
|
@@ -204,7 +225,7 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
204
225
|
let txHash;
|
|
205
226
|
const timer = new Timer();
|
|
206
227
|
|
|
207
|
-
if (await this.
|
|
228
|
+
if (await this.checkIfTxsAreAvailable(block)) {
|
|
208
229
|
this.log.verbose(`Transaction effects of block ${block.number} already published.`, ctx);
|
|
209
230
|
txHash = await this.sendProcessTx(processTxArgs);
|
|
210
231
|
} else {
|
|
@@ -224,7 +245,7 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
224
245
|
|
|
225
246
|
// Tx was mined successfully
|
|
226
247
|
if (receipt.status) {
|
|
227
|
-
const tx = await this.
|
|
248
|
+
const tx = await this.getTransactionStats(txHash);
|
|
228
249
|
const stats: L1PublishBlockStats = {
|
|
229
250
|
...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'),
|
|
230
251
|
...pick(tx!, 'calldataGas', 'calldataSize'),
|
|
@@ -284,13 +305,13 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
284
305
|
|
|
285
306
|
// Tx was mined successfully
|
|
286
307
|
if (receipt.status) {
|
|
287
|
-
const tx = await this.
|
|
308
|
+
const tx = await this.getTransactionStats(txHash);
|
|
288
309
|
const stats: L1PublishProofStats = {
|
|
289
310
|
...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'),
|
|
290
311
|
...pick(tx!, 'calldataGas', 'calldataSize'),
|
|
291
312
|
eventName: 'proof-published-to-l1',
|
|
292
313
|
};
|
|
293
|
-
this.log.info(`Published
|
|
314
|
+
this.log.info(`Published proof to L1 rollup contract`, { ...stats, ...ctx });
|
|
294
315
|
this.metrics.recordSubmitProof(timer.ms(), stats);
|
|
295
316
|
return true;
|
|
296
317
|
}
|
|
@@ -320,13 +341,18 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
320
341
|
this.interrupted = false;
|
|
321
342
|
}
|
|
322
343
|
|
|
344
|
+
async getCurrentArchive(): Promise<Buffer> {
|
|
345
|
+
const archive = await this.rollupContract.read.archive();
|
|
346
|
+
return Buffer.from(archive.replace('0x', ''), 'hex');
|
|
347
|
+
}
|
|
348
|
+
|
|
323
349
|
/**
|
|
324
350
|
* Verifies that the given value of last archive in a block header equals current archive of the rollup contract
|
|
325
351
|
* @param lastArchive - The last archive of the block we wish to publish.
|
|
326
352
|
* @returns Boolean indicating if the hashes are equal.
|
|
327
353
|
*/
|
|
328
354
|
private async checkLastArchiveHash(lastArchive: Buffer): Promise<boolean> {
|
|
329
|
-
const fromChain = await this.
|
|
355
|
+
const fromChain = await this.getCurrentArchive();
|
|
330
356
|
const areSame = lastArchive.equals(fromChain);
|
|
331
357
|
if (!areSame) {
|
|
332
358
|
this.log.debug(`Contract archive: ${fromChain.toString('hex')}`);
|
|
@@ -339,7 +365,19 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
339
365
|
try {
|
|
340
366
|
const size = Object.values(submitProofArgs).reduce((acc, arg) => acc + arg.length, 0);
|
|
341
367
|
this.log.info(`SubmitProof size=${size} bytes`);
|
|
342
|
-
|
|
368
|
+
|
|
369
|
+
const { header, archive, proverId, aggregationObject, proof } = submitProofArgs;
|
|
370
|
+
const args = [
|
|
371
|
+
`0x${header.toString('hex')}`,
|
|
372
|
+
`0x${archive.toString('hex')}`,
|
|
373
|
+
`0x${proverId.toString('hex')}`,
|
|
374
|
+
`0x${aggregationObject.toString('hex')}`,
|
|
375
|
+
`0x${proof.toString('hex')}`,
|
|
376
|
+
] as const;
|
|
377
|
+
|
|
378
|
+
return await this.rollupContract.write.submitBlockRootProof(args, {
|
|
379
|
+
account: this.account,
|
|
380
|
+
});
|
|
343
381
|
} catch (err) {
|
|
344
382
|
this.log.error(`Rollup submit proof failed`, err);
|
|
345
383
|
return undefined;
|
|
@@ -350,7 +388,11 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
350
388
|
while (!this.interrupted) {
|
|
351
389
|
try {
|
|
352
390
|
this.log.info(`TxEffects size=${encodedBody.length} bytes`);
|
|
353
|
-
|
|
391
|
+
const args = [`0x${encodedBody.toString('hex')}`] as const;
|
|
392
|
+
|
|
393
|
+
return await this.availabilityOracleContract.write.publish(args, {
|
|
394
|
+
account: this.account,
|
|
395
|
+
});
|
|
354
396
|
} catch (err) {
|
|
355
397
|
this.log.error(`TxEffects publish failed`, err);
|
|
356
398
|
return undefined;
|
|
@@ -361,7 +403,29 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
361
403
|
private async sendProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
|
|
362
404
|
while (!this.interrupted) {
|
|
363
405
|
try {
|
|
364
|
-
|
|
406
|
+
if (encodedData.attestations) {
|
|
407
|
+
const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
|
|
408
|
+
const args = [
|
|
409
|
+
`0x${encodedData.header.toString('hex')}`,
|
|
410
|
+
`0x${encodedData.archive.toString('hex')}`,
|
|
411
|
+
`0x${encodedData.blockHash.toString('hex')}`,
|
|
412
|
+
attestations,
|
|
413
|
+
] as const;
|
|
414
|
+
|
|
415
|
+
return await this.rollupContract.write.process(args, {
|
|
416
|
+
account: this.account,
|
|
417
|
+
});
|
|
418
|
+
} else {
|
|
419
|
+
const args = [
|
|
420
|
+
`0x${encodedData.header.toString('hex')}`,
|
|
421
|
+
`0x${encodedData.archive.toString('hex')}`,
|
|
422
|
+
`0x${encodedData.blockHash.toString('hex')}`,
|
|
423
|
+
] as const;
|
|
424
|
+
|
|
425
|
+
return await this.rollupContract.write.process(args, {
|
|
426
|
+
account: this.account,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
365
429
|
} catch (err) {
|
|
366
430
|
this.log.error(`Rollup publish failed`, err);
|
|
367
431
|
return undefined;
|
|
@@ -372,7 +436,32 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
372
436
|
private async sendPublishAndProcessTx(encodedData: L1ProcessArgs): Promise<string | undefined> {
|
|
373
437
|
while (!this.interrupted) {
|
|
374
438
|
try {
|
|
375
|
-
|
|
439
|
+
// @note This is quite a sin, but I'm committing war crimes in this code already.
|
|
440
|
+
if (encodedData.attestations) {
|
|
441
|
+
const attestations = encodedData.attestations.map(attest => attest.toViemSignature());
|
|
442
|
+
const args = [
|
|
443
|
+
`0x${encodedData.header.toString('hex')}`,
|
|
444
|
+
`0x${encodedData.archive.toString('hex')}`,
|
|
445
|
+
`0x${encodedData.blockHash.toString('hex')}`,
|
|
446
|
+
attestations,
|
|
447
|
+
`0x${encodedData.body.toString('hex')}`,
|
|
448
|
+
] as const;
|
|
449
|
+
|
|
450
|
+
return await this.rollupContract.write.publishAndProcess(args, {
|
|
451
|
+
account: this.account,
|
|
452
|
+
});
|
|
453
|
+
} else {
|
|
454
|
+
const args = [
|
|
455
|
+
`0x${encodedData.header.toString('hex')}`,
|
|
456
|
+
`0x${encodedData.archive.toString('hex')}`,
|
|
457
|
+
`0x${encodedData.blockHash.toString('hex')}`,
|
|
458
|
+
`0x${encodedData.body.toString('hex')}`,
|
|
459
|
+
] as const;
|
|
460
|
+
|
|
461
|
+
return await this.rollupContract.write.publishAndProcess(args, {
|
|
462
|
+
account: this.account,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
376
465
|
} catch (err) {
|
|
377
466
|
this.log.error(`Rollup publish failed`, err);
|
|
378
467
|
return undefined;
|
|
@@ -380,10 +469,34 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
380
469
|
}
|
|
381
470
|
}
|
|
382
471
|
|
|
383
|
-
|
|
472
|
+
/**
|
|
473
|
+
* Returns a tx receipt if the tx has been mined.
|
|
474
|
+
* @param txHash - Hash of the tx to look for.
|
|
475
|
+
* @returns Undefined if the tx hasn't been mined yet, the receipt otherwise.
|
|
476
|
+
*/
|
|
477
|
+
async getTransactionReceipt(txHash: string): Promise<MinimalTransactionReceipt | undefined> {
|
|
384
478
|
while (!this.interrupted) {
|
|
385
479
|
try {
|
|
386
|
-
|
|
480
|
+
const receipt = await this.publicClient.getTransactionReceipt({
|
|
481
|
+
hash: txHash as Hex,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
if (receipt) {
|
|
485
|
+
if (receipt.transactionHash !== txHash) {
|
|
486
|
+
throw new Error(`Tx hash mismatch: ${receipt.transactionHash} !== ${txHash}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
status: receipt.status === 'success',
|
|
491
|
+
transactionHash: txHash,
|
|
492
|
+
gasUsed: receipt.gasUsed,
|
|
493
|
+
gasPrice: receipt.effectiveGasPrice,
|
|
494
|
+
logs: receipt.logs,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
this.log.debug(`Receipt not found for tx hash ${txHash}`);
|
|
499
|
+
return undefined;
|
|
387
500
|
} catch (err) {
|
|
388
501
|
//this.log.error(`Error getting tx receipt`, err);
|
|
389
502
|
await this.sleepOrInterrupted();
|
|
@@ -395,3 +508,12 @@ export class L1Publisher implements L2BlockReceiver {
|
|
|
395
508
|
await this.interruptibleSleep.sleep(this.sleepTimeMs);
|
|
396
509
|
}
|
|
397
510
|
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Returns cost of calldata usage in Ethereum.
|
|
514
|
+
* @param data - Calldata.
|
|
515
|
+
* @returns 4 for each zero byte, 16 for each nonzero.
|
|
516
|
+
*/
|
|
517
|
+
function getCalldataGasUsage(data: Uint8Array) {
|
|
518
|
+
return data.filter(byte => byte === 0).length * 4 + data.filter(byte => byte !== 0).length * 16;
|
|
519
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type BlockAttestation,
|
|
2
3
|
type L1ToL2MessageSource,
|
|
3
4
|
type L2Block,
|
|
4
5
|
type L2BlockSource,
|
|
5
6
|
type ProcessedTx,
|
|
7
|
+
Signature,
|
|
6
8
|
Tx,
|
|
7
9
|
type TxValidator,
|
|
8
10
|
} from '@aztec/circuit-types';
|
|
@@ -16,11 +18,12 @@ import { Timer, elapsed } from '@aztec/foundation/timer';
|
|
|
16
18
|
import { type P2P } from '@aztec/p2p';
|
|
17
19
|
import { type PublicProcessorFactory } from '@aztec/simulator';
|
|
18
20
|
import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client';
|
|
21
|
+
import { type ValidatorClient } from '@aztec/validator-client';
|
|
19
22
|
import { type WorldStateStatus, type WorldStateSynchronizer } from '@aztec/world-state';
|
|
20
23
|
|
|
21
24
|
import { type BlockBuilderFactory } from '../block_builder/index.js';
|
|
22
25
|
import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
23
|
-
import { type
|
|
26
|
+
import { type L1Publisher } from '../publisher/l1-publisher.js';
|
|
24
27
|
import { type TxValidatorFactory } from '../tx_validator/tx_validator_factory.js';
|
|
25
28
|
import { type SequencerConfig } from './config.js';
|
|
26
29
|
import { SequencerMetrics } from './metrics.js';
|
|
@@ -50,9 +53,11 @@ export class Sequencer {
|
|
|
50
53
|
private allowedInTeardown: AllowedElement[] = [];
|
|
51
54
|
private maxBlockSizeInBytes: number = 1024 * 1024;
|
|
52
55
|
private metrics: SequencerMetrics;
|
|
56
|
+
private isFlushing: boolean = false;
|
|
53
57
|
|
|
54
58
|
constructor(
|
|
55
59
|
private publisher: L1Publisher,
|
|
60
|
+
private validatorClient: ValidatorClient | undefined, // During migration the validator client can be inactive
|
|
56
61
|
private globalsBuilder: GlobalVariableBuilder,
|
|
57
62
|
private p2pClient: P2P,
|
|
58
63
|
private worldState: WorldStateSynchronizer,
|
|
@@ -190,6 +195,10 @@ export class Sequencer {
|
|
|
190
195
|
return;
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
if (this.isFlushing) {
|
|
199
|
+
this.log.verbose(`Flushing all pending txs in new block`);
|
|
200
|
+
}
|
|
201
|
+
|
|
193
202
|
// Compute time elapsed since the previous block
|
|
194
203
|
const lastBlockTime = historicalHeader?.globalVariables.timestamp.toNumber() || 0;
|
|
195
204
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
@@ -199,7 +208,11 @@ export class Sequencer {
|
|
|
199
208
|
);
|
|
200
209
|
|
|
201
210
|
// Do not go forward with new block if not enough time has passed since last block
|
|
202
|
-
if (
|
|
211
|
+
if (
|
|
212
|
+
!this.isFlushing &&
|
|
213
|
+
this.minSecondsBetweenBlocks > 0 &&
|
|
214
|
+
elapsedSinceLastBlock < this.minSecondsBetweenBlocks
|
|
215
|
+
) {
|
|
203
216
|
this.log.debug(
|
|
204
217
|
`Not creating block because not enough time ${this.minSecondsBetweenBlocks} has passed since last block`,
|
|
205
218
|
);
|
|
@@ -212,7 +225,7 @@ export class Sequencer {
|
|
|
212
225
|
const pendingTxs = this.p2pClient.getTxs('pending');
|
|
213
226
|
|
|
214
227
|
// If we haven't hit the maxSecondsBetweenBlocks, we need to have at least minTxsPerBLock txs.
|
|
215
|
-
if (pendingTxs.length < this.minTxsPerBLock) {
|
|
228
|
+
if (!this.isFlushing && pendingTxs.length < this.minTxsPerBLock) {
|
|
216
229
|
if (this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock)) {
|
|
217
230
|
this.log.debug(
|
|
218
231
|
`Creating block with only ${pendingTxs.length} txs as more than ${this.maxSecondsBetweenBlocks}s have passed since last block`,
|
|
@@ -248,7 +261,11 @@ export class Sequencer {
|
|
|
248
261
|
const validTxs = this.takeTxsWithinMaxSize(allValidTxs);
|
|
249
262
|
|
|
250
263
|
// Bail if we don't have enough valid txs
|
|
251
|
-
if (
|
|
264
|
+
if (
|
|
265
|
+
!this.isFlushing &&
|
|
266
|
+
!this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) &&
|
|
267
|
+
validTxs.length < this.minTxsPerBLock
|
|
268
|
+
) {
|
|
252
269
|
this.log.debug(
|
|
253
270
|
`Not creating block because not enough valid txs loaded from the pool (got ${validTxs.length} min ${this.minTxsPerBLock})`,
|
|
254
271
|
);
|
|
@@ -330,7 +347,12 @@ export class Sequencer {
|
|
|
330
347
|
// less txs than the minimum. But that'd cause the entire block to be aborted and retried. Instead, we should
|
|
331
348
|
// go back to the p2p pool and load more txs until we hit our minTxsPerBLock target. Only if there are no txs
|
|
332
349
|
// we should bail.
|
|
333
|
-
if (
|
|
350
|
+
if (
|
|
351
|
+
!this.isFlushing &&
|
|
352
|
+
processedTxs.length === 0 &&
|
|
353
|
+
!this.skipMinTxsPerBlockCheck(elapsedSinceLastBlock) &&
|
|
354
|
+
this.minTxsPerBLock > 0
|
|
355
|
+
) {
|
|
334
356
|
this.log.verbose('No txs processed correctly to build block. Exiting');
|
|
335
357
|
blockBuilder.cancelBlock();
|
|
336
358
|
return;
|
|
@@ -370,6 +392,11 @@ export class Sequencer {
|
|
|
370
392
|
} satisfies L2BlockBuiltStats,
|
|
371
393
|
);
|
|
372
394
|
|
|
395
|
+
if (this.isFlushing) {
|
|
396
|
+
this.log.verbose(`Flushing completed`);
|
|
397
|
+
}
|
|
398
|
+
this.isFlushing = false;
|
|
399
|
+
|
|
373
400
|
try {
|
|
374
401
|
const attestations = await this.collectAttestations(block);
|
|
375
402
|
await this.publishL2Block(block, attestations);
|
|
@@ -377,7 +404,7 @@ export class Sequencer {
|
|
|
377
404
|
this.log.info(
|
|
378
405
|
`Submitted rollup block ${block.number} with ${
|
|
379
406
|
processedTxs.length
|
|
380
|
-
} transactions duration=${workDuration}ms (Submitter: ${await this.publisher.
|
|
407
|
+
} transactions duration=${workDuration}ms (Submitter: ${await this.publisher.getSenderAddress()})`,
|
|
381
408
|
);
|
|
382
409
|
} catch (err) {
|
|
383
410
|
this.metrics.recordFailedBlock();
|
|
@@ -385,7 +412,12 @@ export class Sequencer {
|
|
|
385
412
|
}
|
|
386
413
|
}
|
|
387
414
|
|
|
388
|
-
|
|
415
|
+
/** Forces the sequencer to bypass all time and tx count checks for the next block and build anyway. */
|
|
416
|
+
public flush() {
|
|
417
|
+
this.isFlushing = true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
protected async collectAttestations(block: L2Block): Promise<Signature[] | undefined> {
|
|
389
421
|
// @todo This should collect attestations properly and fix the ordering of them to make sense
|
|
390
422
|
// the current implementation is a PLACEHOLDER and should be nuked from orbit.
|
|
391
423
|
// It is assuming that there will only be ONE (1) validator, so only one attestation
|
|
@@ -399,12 +431,30 @@ export class Sequencer {
|
|
|
399
431
|
// ; ;
|
|
400
432
|
// / \
|
|
401
433
|
// _____________/_ __ \_____________
|
|
402
|
-
if (IS_DEV_NET) {
|
|
434
|
+
if (IS_DEV_NET || !this.validatorClient) {
|
|
403
435
|
return undefined;
|
|
404
436
|
}
|
|
405
437
|
|
|
406
|
-
|
|
407
|
-
|
|
438
|
+
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962): inefficient to have a round trip in here - this should be cached
|
|
439
|
+
const committee = await this.publisher.getCurrentEpochCommittee();
|
|
440
|
+
const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
|
|
441
|
+
|
|
442
|
+
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7974): we do not have transaction[] lists in the block for now
|
|
443
|
+
// Dont do anything with the proposals for now - just collect them
|
|
444
|
+
|
|
445
|
+
const proposal = await this.validatorClient.createBlockProposal(block.header, block.archive.root, []);
|
|
446
|
+
|
|
447
|
+
this.state = SequencerState.PUBLISHING_BLOCK_TO_PEERS;
|
|
448
|
+
this.validatorClient.broadcastBlockProposal(proposal);
|
|
449
|
+
|
|
450
|
+
this.state = SequencerState.WAITING_FOR_ATTESTATIONS;
|
|
451
|
+
const attestations = await this.validatorClient.collectAttestations(
|
|
452
|
+
proposal.header.globalVariables.slotNumber.toBigInt(),
|
|
453
|
+
numberOfRequiredAttestations,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// note: the smart contract requires that the signatures are provided in the order of the committee
|
|
457
|
+
return await orderAttestations(attestations, committee);
|
|
408
458
|
}
|
|
409
459
|
|
|
410
460
|
/**
|
|
@@ -414,9 +464,10 @@ export class Sequencer {
|
|
|
414
464
|
@trackSpan('Sequencer.publishL2Block', block => ({
|
|
415
465
|
[Attributes.BLOCK_NUMBER]: block.number,
|
|
416
466
|
}))
|
|
417
|
-
protected async publishL2Block(block: L2Block, attestations?:
|
|
467
|
+
protected async publishL2Block(block: L2Block, attestations?: Signature[]) {
|
|
418
468
|
// Publishes new block to the network and awaits the tx to be mined
|
|
419
469
|
this.state = SequencerState.PUBLISHING_BLOCK;
|
|
470
|
+
|
|
420
471
|
const publishedL2Block = await this.publisher.processL2Block(block, attestations);
|
|
421
472
|
if (publishedL2Block) {
|
|
422
473
|
this.lastPublishedBlock = block.number;
|
|
@@ -503,6 +554,14 @@ export enum SequencerState {
|
|
|
503
554
|
* Creating a new L2 block. Includes processing public function calls and running rollup circuits. Will move to PUBLISHING_CONTRACT_DATA.
|
|
504
555
|
*/
|
|
505
556
|
CREATING_BLOCK,
|
|
557
|
+
/**
|
|
558
|
+
* Publishing blocks to validator peers. Will move to WAITING_FOR_ATTESTATIONS.
|
|
559
|
+
*/
|
|
560
|
+
PUBLISHING_BLOCK_TO_PEERS,
|
|
561
|
+
/**
|
|
562
|
+
* The block has been published to peers, and we are waiting for attestations. Will move to PUBLISHING_CONTRACT_DATA.
|
|
563
|
+
*/
|
|
564
|
+
WAITING_FOR_ATTESTATIONS,
|
|
506
565
|
/**
|
|
507
566
|
* Sending the tx to L1 with encrypted logs and awaiting it to be mined. Will move back to PUBLISHING_BLOCK once finished.
|
|
508
567
|
*/
|
|
@@ -516,3 +575,30 @@ export enum SequencerState {
|
|
|
516
575
|
*/
|
|
517
576
|
STOPPED,
|
|
518
577
|
}
|
|
578
|
+
|
|
579
|
+
/** Order Attestations
|
|
580
|
+
*
|
|
581
|
+
* Returns attestation signatures in the order of a series of provided ethereum addresses
|
|
582
|
+
* The rollup smart contract expects attestations to appear in the order of the committee
|
|
583
|
+
*
|
|
584
|
+
* @todo: perform this logic within the memory attestation store instead?
|
|
585
|
+
*/
|
|
586
|
+
async function orderAttestations(attestations: BlockAttestation[], orderAddresses: EthAddress[]): Promise<Signature[]> {
|
|
587
|
+
// Create a map of sender addresses to BlockAttestations
|
|
588
|
+
const attestationMap = new Map<string, BlockAttestation>();
|
|
589
|
+
|
|
590
|
+
for (const attestation of attestations) {
|
|
591
|
+
const sender = await attestation.getSender();
|
|
592
|
+
if (sender) {
|
|
593
|
+
attestationMap.set(sender.toString(), attestation);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Create the ordered array based on the orderAddresses, else return an empty signature
|
|
598
|
+
const orderedAttestations = orderAddresses.map(address => {
|
|
599
|
+
const addressString = address.toString();
|
|
600
|
+
return attestationMap.get(addressString)?.signature || Signature.empty();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return orderedAttestations;
|
|
604
|
+
}
|