@aztec/sequencer-client 0.0.1-commit.f2ce05ee → 0.0.1-commit.f81dbcf
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 +23 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +99 -16
- package/dest/config.d.ts +24 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +36 -20
- package/dest/global_variable_builder/global_builder.d.ts +2 -4
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +31 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +101 -42
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +13 -2
- package/dest/publisher/sequencer-publisher.d.ts +16 -7
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +41 -21
- package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +100 -56
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +86 -15
- package/dest/sequencer/sequencer.d.ts +24 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +36 -39
- package/dest/sequencer/timetable.d.ts +4 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +6 -7
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +14 -10
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +47 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +45 -27
- package/src/global_variable_builder/global_builder.ts +1 -1
- package/src/publisher/config.ts +112 -43
- package/src/publisher/sequencer-publisher-factory.ts +23 -6
- package/src/publisher/sequencer-publisher.ts +63 -28
- package/src/sequencer/checkpoint_proposal_job.ts +150 -66
- package/src/sequencer/metrics.ts +92 -18
- package/src/sequencer/sequencer.ts +45 -45
- package/src/sequencer/timetable.ts +7 -7
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +64 -48
- package/src/test/utils.ts +5 -2
|
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
|
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
+
FeeAssetPriceOracle,
|
|
7
8
|
type GovernanceProposerContract,
|
|
8
9
|
type IEmpireBase,
|
|
9
10
|
MULTI_CALL_3_ADDRESS,
|
|
@@ -18,11 +19,11 @@ import {
|
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
21
23
|
MAX_L1_TX_LIMIT,
|
|
22
24
|
type TransactionStats,
|
|
23
25
|
WEI_CONST,
|
|
24
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
26
27
|
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
27
28
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
28
29
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
@@ -32,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
|
32
33
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
33
34
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
34
35
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
36
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
35
37
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
36
38
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
37
39
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
@@ -45,7 +47,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
|
|
|
45
47
|
|
|
46
48
|
import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
47
49
|
|
|
48
|
-
import type {
|
|
50
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
49
51
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
50
52
|
|
|
51
53
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -60,6 +62,8 @@ type L1ProcessArgs = {
|
|
|
60
62
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
61
63
|
/** Attestations and signers signature */
|
|
62
64
|
attestationsAndSignersSignature: Signature;
|
|
65
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
66
|
+
feeAssetPriceModifier: bigint;
|
|
63
67
|
};
|
|
64
68
|
|
|
65
69
|
export const Actions = [
|
|
@@ -112,6 +116,7 @@ export class SequencerPublisher {
|
|
|
112
116
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
117
|
|
|
114
118
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
119
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
115
120
|
|
|
116
121
|
protected log: Logger;
|
|
117
122
|
protected ethereumSlotDuration: bigint;
|
|
@@ -123,13 +128,17 @@ export class SequencerPublisher {
|
|
|
123
128
|
|
|
124
129
|
/** L1 fee analyzer for fisherman mode */
|
|
125
130
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
131
|
+
|
|
132
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
133
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
134
|
+
|
|
126
135
|
// A CALL to a cold address is 2700 gas
|
|
127
136
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
128
137
|
|
|
129
138
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
130
139
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
131
140
|
|
|
132
|
-
public l1TxUtils:
|
|
141
|
+
public l1TxUtils: L1TxUtils;
|
|
133
142
|
public rollupContract: RollupContract;
|
|
134
143
|
public govProposerContract: GovernanceProposerContract;
|
|
135
144
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -140,11 +149,12 @@ export class SequencerPublisher {
|
|
|
140
149
|
protected requests: RequestWithExpiry[] = [];
|
|
141
150
|
|
|
142
151
|
constructor(
|
|
143
|
-
private config:
|
|
152
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
|
|
153
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
144
154
|
deps: {
|
|
145
155
|
telemetry?: TelemetryClient;
|
|
146
156
|
blobClient: BlobClientInterface;
|
|
147
|
-
l1TxUtils:
|
|
157
|
+
l1TxUtils: L1TxUtils;
|
|
148
158
|
rollupContract: RollupContract;
|
|
149
159
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
150
160
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -188,12 +198,27 @@ export class SequencerPublisher {
|
|
|
188
198
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
189
199
|
);
|
|
190
200
|
}
|
|
201
|
+
|
|
202
|
+
// Initialize fee asset price oracle
|
|
203
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
204
|
+
this.l1TxUtils.client,
|
|
205
|
+
this.rollupContract,
|
|
206
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
207
|
+
);
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
public getRollupContract(): RollupContract {
|
|
194
211
|
return this.rollupContract;
|
|
195
212
|
}
|
|
196
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Gets the fee asset price modifier from the oracle.
|
|
216
|
+
* Returns 0n if the oracle query fails.
|
|
217
|
+
*/
|
|
218
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
219
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
220
|
+
}
|
|
221
|
+
|
|
197
222
|
public getSenderAddress() {
|
|
198
223
|
return this.l1TxUtils.getSenderAddress();
|
|
199
224
|
}
|
|
@@ -617,24 +642,8 @@ export class SequencerPublisher {
|
|
|
617
642
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
618
643
|
): Promise<bigint> {
|
|
619
644
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
620
|
-
|
|
621
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
622
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
623
|
-
// so that the committee is recalculated correctly
|
|
624
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
625
|
-
// if (ignoreSignatures) {
|
|
626
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
627
|
-
// if (!committee) {
|
|
628
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
629
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
630
|
-
// }
|
|
631
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
632
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
633
|
-
// );
|
|
634
|
-
// }
|
|
635
|
-
|
|
636
645
|
const blobFields = checkpoint.toBlobFields();
|
|
637
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
646
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
638
647
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
639
648
|
|
|
640
649
|
const args = [
|
|
@@ -642,7 +651,7 @@ export class SequencerPublisher {
|
|
|
642
651
|
header: checkpoint.header.toViem(),
|
|
643
652
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
644
653
|
oracleInput: {
|
|
645
|
-
feeAssetPriceModifier:
|
|
654
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
646
655
|
},
|
|
647
656
|
},
|
|
648
657
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -691,6 +700,32 @@ export class SequencerPublisher {
|
|
|
691
700
|
return false;
|
|
692
701
|
}
|
|
693
702
|
|
|
703
|
+
// Check if payload was already submitted to governance
|
|
704
|
+
const cacheKey = payload.toString();
|
|
705
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
706
|
+
try {
|
|
707
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
708
|
+
const proposed = await retry(
|
|
709
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
710
|
+
'Check if payload was proposed',
|
|
711
|
+
makeBackoff([0, 1, 2]),
|
|
712
|
+
this.log,
|
|
713
|
+
true,
|
|
714
|
+
);
|
|
715
|
+
if (proposed) {
|
|
716
|
+
this.payloadProposedCache.add(cacheKey);
|
|
717
|
+
}
|
|
718
|
+
} catch (err) {
|
|
719
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
725
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
694
729
|
const cachedLastVote = this.lastActions[signalType];
|
|
695
730
|
this.lastActions[signalType] = slotNumber;
|
|
696
731
|
const action = signalType;
|
|
@@ -918,14 +953,15 @@ export class SequencerPublisher {
|
|
|
918
953
|
const checkpointHeader = checkpoint.header;
|
|
919
954
|
|
|
920
955
|
const blobFields = checkpoint.toBlobFields();
|
|
921
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
956
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
922
957
|
|
|
923
|
-
const proposeTxArgs = {
|
|
958
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
924
959
|
header: checkpointHeader,
|
|
925
960
|
archive: checkpoint.archive.root.toBuffer(),
|
|
926
961
|
blobs,
|
|
927
962
|
attestationsAndSigners,
|
|
928
963
|
attestationsAndSignersSignature,
|
|
964
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
929
965
|
};
|
|
930
966
|
|
|
931
967
|
let ts: bigint;
|
|
@@ -1113,8 +1149,7 @@ export class SequencerPublisher {
|
|
|
1113
1149
|
header: encodedData.header.toViem(),
|
|
1114
1150
|
archive: toHex(encodedData.archive),
|
|
1115
1151
|
oracleInput: {
|
|
1116
|
-
|
|
1117
|
-
feeAssetPriceModifier: 0n,
|
|
1152
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1118
1153
|
},
|
|
1119
1154
|
},
|
|
1120
1155
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1140,7 +1175,7 @@ export class SequencerPublisher {
|
|
|
1140
1175
|
readonly header: ViemHeader;
|
|
1141
1176
|
readonly archive: `0x${string}`;
|
|
1142
1177
|
readonly oracleInput: {
|
|
1143
|
-
readonly feeAssetPriceModifier:
|
|
1178
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1144
1179
|
};
|
|
1145
1180
|
},
|
|
1146
1181
|
ViemCommitteeAttestations,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
3
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
2
|
import {
|
|
5
3
|
BlockNumber,
|
|
@@ -9,6 +7,11 @@ import {
|
|
|
9
7
|
SlotNumber,
|
|
10
8
|
} from '@aztec/foundation/branded-types';
|
|
11
9
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import {
|
|
11
|
+
flipSignature,
|
|
12
|
+
generateRecoverableSignature,
|
|
13
|
+
generateUnrecoverableSignature,
|
|
14
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
12
15
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
13
16
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
14
17
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
@@ -27,18 +30,18 @@ import {
|
|
|
27
30
|
type L2BlockSource,
|
|
28
31
|
MaliciousCommitteeAttestationsAndSigners,
|
|
29
32
|
} from '@aztec/stdlib/block';
|
|
30
|
-
import type
|
|
33
|
+
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
31
34
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
32
35
|
import { Gas } from '@aztec/stdlib/gas';
|
|
33
36
|
import {
|
|
34
|
-
|
|
37
|
+
InsufficientValidTxsError,
|
|
35
38
|
type PublicProcessorLimits,
|
|
36
39
|
type ResolvedSequencerConfig,
|
|
37
40
|
type WorldStateSynchronizer,
|
|
38
41
|
} from '@aztec/stdlib/interfaces/server';
|
|
39
42
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
40
43
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
41
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
44
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
42
45
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
43
46
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
44
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -129,7 +132,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
129
132
|
await Promise.all(votesPromises);
|
|
130
133
|
|
|
131
134
|
if (checkpoint) {
|
|
132
|
-
this.metrics.
|
|
135
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
@@ -186,18 +189,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
186
189
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
187
190
|
|
|
188
191
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
189
|
-
const
|
|
190
|
-
c => c.
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
193
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
194
|
+
.map(c => c.checkpointOutHash);
|
|
195
|
+
|
|
196
|
+
// Get the fee asset price modifier from the oracle
|
|
197
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
193
198
|
|
|
194
199
|
// Create a long-lived forked world state for the checkpoint builder
|
|
195
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
200
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
196
201
|
|
|
197
202
|
// Create checkpoint builder for the entire slot
|
|
198
203
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
199
204
|
this.checkpointNumber,
|
|
200
205
|
checkpointGlobalVariables,
|
|
206
|
+
feeAssetPriceModifier,
|
|
201
207
|
l1ToL2Messages,
|
|
202
208
|
previousCheckpointOutHashes,
|
|
203
209
|
fork,
|
|
@@ -217,6 +223,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
217
223
|
|
|
218
224
|
let blocksInCheckpoint: L2Block[] = [];
|
|
219
225
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
226
|
+
const checkpointBuildTimer = new Timer();
|
|
220
227
|
|
|
221
228
|
try {
|
|
222
229
|
// Main loop: build blocks for the checkpoint
|
|
@@ -244,11 +251,44 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
244
251
|
return undefined;
|
|
245
252
|
}
|
|
246
253
|
|
|
254
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
255
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
256
|
+
this.log.warn(
|
|
257
|
+
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
258
|
+
{ slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
259
|
+
);
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
248
264
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
249
265
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
250
266
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
251
267
|
|
|
268
|
+
// Final validation round for the checkpoint before we propose it, just for safety
|
|
269
|
+
try {
|
|
270
|
+
validateCheckpoint(checkpoint, {
|
|
271
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
272
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
273
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
274
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
275
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
|
|
279
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
280
|
+
});
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Record checkpoint-level build metrics
|
|
285
|
+
this.metrics.recordCheckpointBuild(
|
|
286
|
+
checkpointBuildTimer.ms(),
|
|
287
|
+
blocksInCheckpoint.length,
|
|
288
|
+
checkpoint.getStats().txCount,
|
|
289
|
+
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
290
|
+
);
|
|
291
|
+
|
|
252
292
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
253
293
|
if (this.config.fishermanMode) {
|
|
254
294
|
this.log.info(
|
|
@@ -275,6 +315,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
275
315
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
276
316
|
checkpoint.header,
|
|
277
317
|
checkpoint.archive.root,
|
|
318
|
+
feeAssetPriceModifier,
|
|
278
319
|
lastBlock,
|
|
279
320
|
this.proposer,
|
|
280
321
|
checkpointProposalOptions,
|
|
@@ -313,6 +354,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
313
354
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
314
355
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
315
356
|
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
357
|
+
|
|
358
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
359
|
+
if (
|
|
360
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
361
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
362
|
+
) {
|
|
363
|
+
const result = Math.max(0, randomInt(100));
|
|
364
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
365
|
+
this.log.warn(
|
|
366
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
367
|
+
);
|
|
368
|
+
return checkpoint;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
316
372
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
317
373
|
txTimeoutAt,
|
|
318
374
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -347,9 +403,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
347
403
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
348
404
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
349
405
|
|
|
350
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
351
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
352
|
-
|
|
353
406
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
354
407
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
355
408
|
|
|
@@ -382,7 +435,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
382
435
|
blockNumber,
|
|
383
436
|
indexWithinCheckpoint,
|
|
384
437
|
txHashesAlreadyIncluded,
|
|
385
|
-
remainingBlobFields,
|
|
386
438
|
});
|
|
387
439
|
|
|
388
440
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -408,12 +460,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
408
460
|
break;
|
|
409
461
|
}
|
|
410
462
|
|
|
411
|
-
const { block, usedTxs
|
|
463
|
+
const { block, usedTxs } = buildResult;
|
|
412
464
|
blocksInCheckpoint.push(block);
|
|
413
465
|
|
|
414
|
-
// Update remaining blob fields for the next block
|
|
415
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
416
|
-
|
|
417
466
|
// Sync the proposed block to the archiver to make it available
|
|
418
467
|
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
419
468
|
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
@@ -481,18 +530,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
481
530
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
482
531
|
buildDeadline: Date | undefined;
|
|
483
532
|
txHashesAlreadyIncluded: Set<string>;
|
|
484
|
-
remainingBlobFields: number;
|
|
485
533
|
},
|
|
486
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
487
|
-
const {
|
|
488
|
-
|
|
489
|
-
forceCreate,
|
|
490
|
-
blockNumber,
|
|
491
|
-
indexWithinCheckpoint,
|
|
492
|
-
buildDeadline,
|
|
493
|
-
txHashesAlreadyIncluded,
|
|
494
|
-
remainingBlobFields,
|
|
495
|
-
} = opts;
|
|
534
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
535
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
536
|
+
opts;
|
|
496
537
|
|
|
497
538
|
this.log.verbose(
|
|
498
539
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -501,8 +542,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
501
542
|
|
|
502
543
|
try {
|
|
503
544
|
// Wait until we have enough txs to build the block
|
|
504
|
-
const minTxs = this.
|
|
505
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
545
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
506
546
|
if (!canStartBuilding) {
|
|
507
547
|
this.log.warn(
|
|
508
548
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -516,7 +556,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
516
556
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
517
557
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
518
558
|
const pendingTxs = filter(
|
|
519
|
-
this.p2pClient.
|
|
559
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
520
560
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
521
561
|
);
|
|
522
562
|
|
|
@@ -526,19 +566,24 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
526
566
|
);
|
|
527
567
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
528
568
|
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const blockBuilderOptions: PublicProcessorLimits = {
|
|
569
|
+
// Per-block limits derived at startup by computeBlockLimits(), further capped
|
|
570
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
571
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
572
|
+
const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
|
|
573
|
+
const blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number } = {
|
|
534
574
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
575
|
+
maxBlockGas:
|
|
576
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
577
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
578
|
+
: undefined,
|
|
538
579
|
deadline: buildDeadline,
|
|
580
|
+
isBuildingProposal: true,
|
|
581
|
+
minValidTxs,
|
|
539
582
|
};
|
|
540
583
|
|
|
541
|
-
// Actually build the block by executing txs
|
|
584
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
585
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
586
|
+
// updated for blocks that will be discarded.
|
|
542
587
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
543
588
|
checkpointBuilder,
|
|
544
589
|
pendingTxs,
|
|
@@ -550,14 +595,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
550
595
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
551
596
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
552
597
|
|
|
553
|
-
|
|
554
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
555
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
556
|
-
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
557
|
-
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
598
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
558
599
|
this.log.warn(
|
|
559
600
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
560
|
-
{
|
|
601
|
+
{
|
|
602
|
+
slot: this.slot,
|
|
603
|
+
blockNumber,
|
|
604
|
+
numTxs: buildResult.processedCount,
|
|
605
|
+
indexWithinCheckpoint,
|
|
606
|
+
minValidTxs,
|
|
607
|
+
},
|
|
561
608
|
);
|
|
562
609
|
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
563
610
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
@@ -565,7 +612,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
565
612
|
}
|
|
566
613
|
|
|
567
614
|
// Block creation succeeded, emit stats and metrics
|
|
568
|
-
const {
|
|
615
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
569
616
|
|
|
570
617
|
const blockStats = {
|
|
571
618
|
eventName: 'l2-block-built',
|
|
@@ -576,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
576
623
|
|
|
577
624
|
const blockHash = await block.hash();
|
|
578
625
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
579
|
-
const manaPerSec =
|
|
626
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
580
627
|
|
|
581
628
|
this.log.info(
|
|
582
629
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -584,9 +631,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
584
631
|
);
|
|
585
632
|
|
|
586
633
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
587
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
634
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
588
635
|
|
|
589
|
-
return { block, usedTxs
|
|
636
|
+
return { block, usedTxs };
|
|
590
637
|
} catch (err: any) {
|
|
591
638
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
592
639
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -596,13 +643,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
596
643
|
}
|
|
597
644
|
}
|
|
598
645
|
|
|
599
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
646
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
600
647
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
601
648
|
checkpointBuilder: CheckpointBuilder,
|
|
602
649
|
pendingTxs: AsyncIterable<Tx>,
|
|
603
650
|
blockNumber: BlockNumber,
|
|
604
651
|
blockTimestamp: bigint,
|
|
605
|
-
blockBuilderOptions: PublicProcessorLimits,
|
|
652
|
+
blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number },
|
|
606
653
|
) {
|
|
607
654
|
try {
|
|
608
655
|
const workTimer = new Timer();
|
|
@@ -610,8 +657,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
610
657
|
const blockBuildDuration = workTimer.ms();
|
|
611
658
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
612
659
|
} catch (err: unknown) {
|
|
613
|
-
if (isErrorClass(err,
|
|
614
|
-
return {
|
|
660
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
661
|
+
return {
|
|
662
|
+
failedTxs: err.failedTxs,
|
|
663
|
+
processedCount: err.processedCount,
|
|
664
|
+
status: 'insufficient-valid-txs' as const,
|
|
665
|
+
};
|
|
615
666
|
}
|
|
616
667
|
throw err;
|
|
617
668
|
}
|
|
@@ -624,7 +675,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
624
675
|
blockNumber: BlockNumber;
|
|
625
676
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
626
677
|
buildDeadline: Date | undefined;
|
|
627
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
678
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
628
679
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
629
680
|
|
|
630
681
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -641,7 +692,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
641
692
|
// If we're past deadline, or we have no deadline, give up
|
|
642
693
|
const now = this.dateProvider.nowAsDate();
|
|
643
694
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
644
|
-
return { canStartBuilding: false, availableTxs
|
|
695
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
645
696
|
}
|
|
646
697
|
|
|
647
698
|
// Wait a bit before checking again
|
|
@@ -654,7 +705,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
654
705
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
655
706
|
}
|
|
656
707
|
|
|
657
|
-
return { canStartBuilding: true, availableTxs };
|
|
708
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
658
709
|
}
|
|
659
710
|
|
|
660
711
|
/**
|
|
@@ -706,11 +757,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
706
757
|
|
|
707
758
|
collectedAttestationsCount = attestations.length;
|
|
708
759
|
|
|
760
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
761
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
762
|
+
const trimmed = trimAttestations(
|
|
763
|
+
attestations,
|
|
764
|
+
numberOfRequiredAttestations,
|
|
765
|
+
this.attestorAddress,
|
|
766
|
+
localAddresses,
|
|
767
|
+
);
|
|
768
|
+
if (trimmed.length < attestations.length) {
|
|
769
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
770
|
+
}
|
|
771
|
+
|
|
709
772
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
710
|
-
const sorted = orderAttestations(
|
|
773
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
711
774
|
|
|
712
775
|
// Manipulate the attestations if we've been configured to do so
|
|
713
|
-
if (
|
|
776
|
+
if (
|
|
777
|
+
this.config.injectFakeAttestation ||
|
|
778
|
+
this.config.injectHighSValueAttestation ||
|
|
779
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
780
|
+
this.config.shuffleAttestationOrdering
|
|
781
|
+
) {
|
|
714
782
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
715
783
|
}
|
|
716
784
|
|
|
@@ -739,7 +807,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
739
807
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
740
808
|
);
|
|
741
809
|
|
|
742
|
-
if (
|
|
810
|
+
if (
|
|
811
|
+
this.config.injectFakeAttestation ||
|
|
812
|
+
this.config.injectHighSValueAttestation ||
|
|
813
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
814
|
+
) {
|
|
743
815
|
// Find non-empty attestations that are not from the proposer
|
|
744
816
|
const nonProposerIndices: number[] = [];
|
|
745
817
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -749,8 +821,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
749
821
|
}
|
|
750
822
|
if (nonProposerIndices.length > 0) {
|
|
751
823
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
752
|
-
this.
|
|
753
|
-
|
|
824
|
+
if (this.config.injectHighSValueAttestation) {
|
|
825
|
+
this.log.warn(
|
|
826
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
827
|
+
);
|
|
828
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
829
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
830
|
+
this.log.warn(
|
|
831
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
832
|
+
);
|
|
833
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
834
|
+
} else {
|
|
835
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
836
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
837
|
+
}
|
|
754
838
|
}
|
|
755
839
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
756
840
|
}
|
|
@@ -779,7 +863,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
779
863
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
780
864
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
781
865
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
782
|
-
await this.p2pClient.
|
|
866
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
783
867
|
}
|
|
784
868
|
|
|
785
869
|
/**
|
|
@@ -821,7 +905,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
821
905
|
slot: this.slot,
|
|
822
906
|
feeAnalysisId: feeAnalysis?.id,
|
|
823
907
|
});
|
|
824
|
-
this.metrics.
|
|
908
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
825
909
|
}
|
|
826
910
|
|
|
827
911
|
this.publisher.clearPendingRequests();
|