@aztec/stdlib 4.0.4-rc.5 → 4.0.4-rc.7

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.
Files changed (47) hide show
  1. package/dest/block/l2_block.d.ts +9 -1
  2. package/dest/block/l2_block.d.ts.map +1 -1
  3. package/dest/block/l2_block.js +12 -2
  4. package/dest/checkpoint/checkpoint.d.ts +2 -1
  5. package/dest/checkpoint/checkpoint.d.ts.map +1 -1
  6. package/dest/checkpoint/checkpoint.js +9 -4
  7. package/dest/checkpoint/index.d.ts +2 -1
  8. package/dest/checkpoint/index.d.ts.map +1 -1
  9. package/dest/checkpoint/index.js +1 -0
  10. package/dest/checkpoint/validate.d.ts +36 -0
  11. package/dest/checkpoint/validate.d.ts.map +1 -0
  12. package/dest/checkpoint/validate.js +120 -0
  13. package/dest/config/sequencer-config.d.ts +2 -4
  14. package/dest/config/sequencer-config.d.ts.map +1 -1
  15. package/dest/config/sequencer-config.js +1 -3
  16. package/dest/interfaces/aztec-node-admin.d.ts +7 -4
  17. package/dest/interfaces/aztec-node-admin.d.ts.map +1 -1
  18. package/dest/interfaces/block-builder.d.ts +11 -5
  19. package/dest/interfaces/block-builder.d.ts.map +1 -1
  20. package/dest/interfaces/block-builder.js +6 -1
  21. package/dest/interfaces/configs.d.ts +12 -7
  22. package/dest/interfaces/configs.d.ts.map +1 -1
  23. package/dest/interfaces/configs.js +2 -1
  24. package/dest/kernel/private_kernel_tail_circuit_public_inputs.d.ts +2 -1
  25. package/dest/kernel/private_kernel_tail_circuit_public_inputs.d.ts.map +1 -1
  26. package/dest/kernel/private_kernel_tail_circuit_public_inputs.js +4 -0
  27. package/dest/tests/mocks.d.ts +4 -2
  28. package/dest/tests/mocks.d.ts.map +1 -1
  29. package/dest/tests/mocks.js +13 -8
  30. package/dest/tx/block_header.d.ts +3 -1
  31. package/dest/tx/block_header.d.ts.map +1 -1
  32. package/dest/tx/block_header.js +4 -0
  33. package/dest/tx/tx.d.ts +6 -5
  34. package/dest/tx/tx.d.ts.map +1 -1
  35. package/dest/tx/tx.js +18 -6
  36. package/package.json +9 -9
  37. package/src/block/l2_block.ts +13 -1
  38. package/src/checkpoint/checkpoint.ts +12 -3
  39. package/src/checkpoint/index.ts +1 -0
  40. package/src/checkpoint/validate.ts +230 -0
  41. package/src/config/sequencer-config.ts +2 -5
  42. package/src/interfaces/block-builder.ts +24 -4
  43. package/src/interfaces/configs.ts +12 -4
  44. package/src/kernel/private_kernel_tail_circuit_public_inputs.ts +9 -0
  45. package/src/tests/mocks.ts +18 -7
  46. package/src/tx/block_header.ts +6 -0
  47. package/src/tx/tx.ts +20 -11
@@ -6,7 +6,7 @@ import {
6
6
  IndexWithinCheckpoint,
7
7
  SlotNumber,
8
8
  } from '@aztec/foundation/branded-types';
9
- import { sum } from '@aztec/foundation/collection';
9
+ import { pick, sum } from '@aztec/foundation/collection';
10
10
  import { Fr } from '@aztec/foundation/curves/bn254';
11
11
  import { BufferReader, serializeSignedBigInt, serializeToBuffer } from '@aztec/foundation/serialize';
12
12
  import type { FieldsOf } from '@aztec/foundation/types';
@@ -152,10 +152,12 @@ export class Checkpoint {
152
152
  startBlockNumber?: number;
153
153
  previousArchive?: AppendOnlyTreeSnapshot;
154
154
  feeAssetPriceModifier?: bigint;
155
+ archive?: AppendOnlyTreeSnapshot;
155
156
  } & Partial<Parameters<typeof CheckpointHeader.random>[0]> &
156
157
  Partial<Parameters<typeof L2Block.random>[1]> = {},
157
158
  ) {
158
- const header = CheckpointHeader.random(options);
159
+ const headerOptions = previousArchive ? { lastArchiveRoot: previousArchive.root, ...options } : options;
160
+ const header = CheckpointHeader.random(headerOptions);
159
161
 
160
162
  // Create blocks sequentially to chain archive roots properly.
161
163
  // Each block's header.lastArchive must equal the previous block's archive.
@@ -166,11 +168,18 @@ export class Checkpoint {
166
168
  indexWithinCheckpoint: IndexWithinCheckpoint(i),
167
169
  ...options,
168
170
  ...(lastArchive ? { lastArchive } : {}),
171
+ ...pick(header, 'slotNumber', 'timestamp', 'coinbase', 'feeRecipient', 'gasFees'),
169
172
  });
170
173
  lastArchive = block.archive;
171
174
  blocks.push(block);
172
175
  }
173
176
 
174
- return new Checkpoint(AppendOnlyTreeSnapshot.random(), header, blocks, checkpointNumber, feeAssetPriceModifier);
177
+ return new Checkpoint(
178
+ options.archive ?? AppendOnlyTreeSnapshot.random(),
179
+ header,
180
+ blocks,
181
+ checkpointNumber,
182
+ feeAssetPriceModifier,
183
+ );
175
184
  }
176
185
  }
@@ -2,3 +2,4 @@ export * from './checkpoint.js';
2
2
  export * from './checkpoint_data.js';
3
3
  export * from './checkpoint_info.js';
4
4
  export * from './published_checkpoint.js';
5
+ export * from './validate.js';
@@ -0,0 +1,230 @@
1
+ import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT } from '@aztec/constants';
2
+ import type { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
3
+ import { sum } from '@aztec/foundation/collection';
4
+
5
+ import { MAX_BLOCKS_PER_CHECKPOINT } from '../deserialization/index.js';
6
+ import type { Checkpoint } from './checkpoint.js';
7
+
8
+ export class CheckpointValidationError extends Error {
9
+ constructor(
10
+ message: string,
11
+ public readonly checkpointNumber: CheckpointNumber,
12
+ public readonly slot: SlotNumber,
13
+ ) {
14
+ super(message);
15
+ this.name = 'CheckpointValidationError';
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Validates a checkpoint. Throws a CheckpointValidationError if any validation fails.
21
+ * - Validates structural integrity (non-empty, block count, sequential numbers, archive chaining, slot consistency)
22
+ * - Validates checkpoint blob field count against maxBlobFields limit
23
+ * - Validates total L2 gas used by checkpoint blocks against the Rollup contract mana limit
24
+ * - Validates total DA gas used by checkpoint blocks against MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT
25
+ * - Validates individual block L2 gas and DA gas against maxL2BlockGas and maxDABlockGas limits
26
+ */
27
+ export function validateCheckpoint(
28
+ checkpoint: Checkpoint,
29
+ opts: {
30
+ rollupManaLimit?: number;
31
+ maxL2BlockGas?: number;
32
+ maxDABlockGas?: number;
33
+ maxTxsPerCheckpoint?: number;
34
+ maxTxsPerBlock?: number;
35
+ },
36
+ ): void {
37
+ validateCheckpointStructure(checkpoint);
38
+ validateCheckpointLimits(checkpoint, opts);
39
+ validateCheckpointBlocksGasLimits(checkpoint, opts);
40
+ }
41
+
42
+ /**
43
+ * Validates structural integrity of a checkpoint.
44
+ * - Non-empty block list
45
+ * - Block count within MAX_BLOCKS_PER_CHECKPOINT
46
+ * - Checkpoint slot matches the first block's slot
47
+ * - Checkpoint lastArchiveRoot matches the first block's lastArchive root
48
+ * - Sequential block numbers without gaps
49
+ * - Sequential indexWithinCheckpoint starting at 0
50
+ * - Archive root chaining between consecutive blocks
51
+ * - Consistent slot number across all blocks
52
+ * - Global variables (slot, timestamp, coinbase, feeRecipient, gasFees) match checkpoint header for each block
53
+ */
54
+ export function validateCheckpointStructure(checkpoint: Checkpoint): void {
55
+ const { blocks, number, slot } = checkpoint;
56
+
57
+ if (blocks.length === 0) {
58
+ throw new CheckpointValidationError('Checkpoint has no blocks', number, slot);
59
+ }
60
+
61
+ if (blocks.length > MAX_BLOCKS_PER_CHECKPOINT) {
62
+ throw new CheckpointValidationError(
63
+ `Checkpoint has ${blocks.length} blocks, exceeding limit of ${MAX_BLOCKS_PER_CHECKPOINT}`,
64
+ number,
65
+ slot,
66
+ );
67
+ }
68
+
69
+ const firstBlock = blocks[0];
70
+
71
+ if (!checkpoint.header.lastArchiveRoot.equals(firstBlock.header.lastArchive.root)) {
72
+ throw new CheckpointValidationError(
73
+ `Checkpoint lastArchiveRoot does not match first block's lastArchive root`,
74
+ number,
75
+ slot,
76
+ );
77
+ }
78
+
79
+ for (let i = 0; i < blocks.length; i++) {
80
+ const block = blocks[i];
81
+
82
+ if (block.indexWithinCheckpoint !== i) {
83
+ throw new CheckpointValidationError(
84
+ `Block at index ${i} has indexWithinCheckpoint ${block.indexWithinCheckpoint}, expected ${i}`,
85
+ number,
86
+ slot,
87
+ );
88
+ }
89
+
90
+ if (block.slot !== slot) {
91
+ throw new CheckpointValidationError(
92
+ `Block ${block.number} has slot ${block.slot}, expected ${slot} (all blocks must share the same slot)`,
93
+ number,
94
+ slot,
95
+ );
96
+ }
97
+
98
+ if (!checkpoint.header.matchesGlobalVariables(block.header.globalVariables)) {
99
+ throw new CheckpointValidationError(
100
+ `Block ${block.number} global variables (slot, timestamp, coinbase, feeRecipient, gasFees) do not match checkpoint header`,
101
+ number,
102
+ slot,
103
+ );
104
+ }
105
+
106
+ if (i > 0) {
107
+ const prev = blocks[i - 1];
108
+ if (block.number !== prev.number + 1) {
109
+ throw new CheckpointValidationError(
110
+ `Block numbers are not sequential: block at index ${i - 1} has number ${prev.number}, block at index ${i} has number ${block.number}`,
111
+ number,
112
+ slot,
113
+ );
114
+ }
115
+
116
+ if (!block.header.lastArchive.root.equals(prev.archive.root)) {
117
+ throw new CheckpointValidationError(
118
+ `Block ${block.number} lastArchive root does not match archive root of block ${prev.number}`,
119
+ number,
120
+ slot,
121
+ );
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ /** Validates checkpoint blocks gas limits */
128
+ function validateCheckpointBlocksGasLimits(
129
+ checkpoint: Checkpoint,
130
+ opts: {
131
+ maxL2BlockGas?: number;
132
+ maxDABlockGas?: number;
133
+ maxTxsPerBlock?: number;
134
+ },
135
+ ): void {
136
+ const { maxL2BlockGas, maxDABlockGas, maxTxsPerBlock } = opts;
137
+
138
+ if (maxL2BlockGas !== undefined) {
139
+ for (const block of checkpoint.blocks) {
140
+ const blockL2Gas = block.header.totalManaUsed.toNumber();
141
+ if (blockL2Gas > maxL2BlockGas) {
142
+ throw new CheckpointValidationError(
143
+ `Block ${block.number} in checkpoint has L2 gas used ${blockL2Gas} exceeding limit of ${maxL2BlockGas}`,
144
+ checkpoint.number,
145
+ checkpoint.slot,
146
+ );
147
+ }
148
+ }
149
+ }
150
+
151
+ if (maxDABlockGas !== undefined) {
152
+ for (const block of checkpoint.blocks) {
153
+ const blockDAGas = block.computeDAGasUsed();
154
+ if (blockDAGas > maxDABlockGas) {
155
+ throw new CheckpointValidationError(
156
+ `Block ${block.number} in checkpoint has DA gas used ${blockDAGas} exceeding limit of ${maxDABlockGas}`,
157
+ checkpoint.number,
158
+ checkpoint.slot,
159
+ );
160
+ }
161
+ }
162
+ }
163
+
164
+ if (maxTxsPerBlock !== undefined) {
165
+ for (const block of checkpoint.blocks) {
166
+ const blockTxCount = block.body.txEffects.length;
167
+ if (blockTxCount > maxTxsPerBlock) {
168
+ throw new CheckpointValidationError(
169
+ `Block ${block.number} in checkpoint has ${blockTxCount} txs exceeding limit of ${maxTxsPerBlock}`,
170
+ checkpoint.number,
171
+ checkpoint.slot,
172
+ );
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ /** Validates checkpoint max blob fields, gas limits, and tx limits */
179
+ function validateCheckpointLimits(
180
+ checkpoint: Checkpoint,
181
+ opts: {
182
+ rollupManaLimit?: number;
183
+ maxTxsPerCheckpoint?: number;
184
+ },
185
+ ): void {
186
+ const { rollupManaLimit, maxTxsPerCheckpoint } = opts;
187
+
188
+ const maxBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB;
189
+ const maxDAGas = MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT;
190
+
191
+ if (rollupManaLimit !== undefined) {
192
+ const checkpointMana = sum(checkpoint.blocks.map(block => block.header.totalManaUsed.toNumber()));
193
+ if (checkpointMana > rollupManaLimit) {
194
+ throw new CheckpointValidationError(
195
+ `Checkpoint mana cost ${checkpointMana} exceeds rollup limit of ${rollupManaLimit}`,
196
+ checkpoint.number,
197
+ checkpoint.slot,
198
+ );
199
+ }
200
+ }
201
+
202
+ const checkpointDAGas = sum(checkpoint.blocks.map(block => block.computeDAGasUsed()));
203
+ if (checkpointDAGas > maxDAGas) {
204
+ throw new CheckpointValidationError(
205
+ `Checkpoint DA gas cost ${checkpointDAGas} exceeds limit of ${maxDAGas}`,
206
+ checkpoint.number,
207
+ checkpoint.slot,
208
+ );
209
+ }
210
+
211
+ const checkpointBlobFields = checkpoint.toBlobFields().length;
212
+ if (checkpointBlobFields > maxBlobFields) {
213
+ throw new CheckpointValidationError(
214
+ `Checkpoint blob field count ${checkpointBlobFields} exceeds limit of ${maxBlobFields}`,
215
+ checkpoint.number,
216
+ checkpoint.slot,
217
+ );
218
+ }
219
+
220
+ if (maxTxsPerCheckpoint !== undefined) {
221
+ const checkpointTxCount = sum(checkpoint.blocks.map(block => block.body.txEffects.length));
222
+ if (checkpointTxCount > maxTxsPerCheckpoint) {
223
+ throw new CheckpointValidationError(
224
+ `Checkpoint tx count ${checkpointTxCount} exceeds limit of ${maxTxsPerCheckpoint}`,
225
+ checkpoint.number,
226
+ checkpoint.slot,
227
+ );
228
+ }
229
+ }
230
+ }
@@ -1,10 +1,7 @@
1
- import { type ConfigMappingsType, numberConfigHelper } from '@aztec/foundation/config';
1
+ import type { ConfigMappingsType } from '@aztec/foundation/config';
2
2
 
3
3
  import type { SequencerConfig } from '../interfaces/configs.js';
4
4
 
5
- /** Default maximum number of transactions per block. */
6
- export const DEFAULT_MAX_TXS_PER_BLOCK = 32;
7
-
8
5
  /**
9
6
  * Partial sequencer config mappings for fields that need to be shared across packages.
10
7
  * The full sequencer config mappings remain in sequencer-client, but shared fields
@@ -32,6 +29,6 @@ export const sharedSequencerConfigMappings: ConfigMappingsType<
32
29
  maxTxsPerBlock: {
33
30
  env: 'SEQ_MAX_TX_PER_BLOCK',
34
31
  description: 'The maximum number of txs to include in a block.',
35
- ...numberConfigHelper(DEFAULT_MAX_TXS_PER_BLOCK),
32
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
36
33
  },
37
34
  };
@@ -36,11 +36,16 @@ export interface IBlockFactory extends ProcessedTxHandler {
36
36
  }
37
37
 
38
38
  export interface PublicProcessorLimits {
39
+ /** Maximum number of txs to process. */
39
40
  maxTransactions?: number;
40
- maxBlockSize?: number;
41
+ /** L2 and DA gas limits. */
41
42
  maxBlockGas?: Gas;
43
+ /** Maximum number of blob fields allowed. */
42
44
  maxBlobFields?: number;
45
+ /** Deadline for processing the txs. Processor will stop as soon as it hits this time. */
43
46
  deadline?: Date;
47
+ /** Whether this processor is building a proposal (as opposed to re-executing one). Skipping txs due to gas or blob limits is only done during proposal building. */
48
+ isBuildingProposal?: boolean;
44
49
  }
45
50
 
46
51
  export interface PublicProcessorValidator {
@@ -50,7 +55,19 @@ export interface PublicProcessorValidator {
50
55
 
51
56
  export type FullNodeBlockBuilderConfig = Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'> &
52
57
  Pick<ChainConfig, 'l1ChainId' | 'rollupVersion'> &
53
- Pick<SequencerConfig, 'txPublicSetupAllowList' | 'fakeProcessingDelayPerTxMs' | 'fakeThrowAfterProcessingTxCount'>;
58
+ Pick<
59
+ SequencerConfig,
60
+ | 'txPublicSetupAllowList'
61
+ | 'fakeProcessingDelayPerTxMs'
62
+ | 'fakeThrowAfterProcessingTxCount'
63
+ | 'maxTxsPerBlock'
64
+ | 'maxTxsPerCheckpoint'
65
+ | 'maxL2BlockGas'
66
+ | 'maxDABlockGas'
67
+ > & {
68
+ /** Total L2 gas (mana) allowed per checkpoint. Fetched from L1 getManaLimit(). */
69
+ rollupManaLimit: number;
70
+ };
54
71
 
55
72
  export const FullNodeBlockBuilderConfigKeys: (keyof FullNodeBlockBuilderConfig)[] = [
56
73
  'l1GenesisTime',
@@ -60,6 +77,11 @@ export const FullNodeBlockBuilderConfigKeys: (keyof FullNodeBlockBuilderConfig)[
60
77
  'txPublicSetupAllowList',
61
78
  'fakeProcessingDelayPerTxMs',
62
79
  'fakeThrowAfterProcessingTxCount',
80
+ 'maxTxsPerBlock',
81
+ 'maxTxsPerCheckpoint',
82
+ 'maxL2BlockGas',
83
+ 'maxDABlockGas',
84
+ 'rollupManaLimit',
63
85
  ] as const;
64
86
 
65
87
  /** Thrown when no valid transactions are available to include in a block after processing, and this is not the first block in a checkpoint. */
@@ -73,12 +95,10 @@ export class NoValidTxsError extends Error {
73
95
  /** Result of building a block within a checkpoint. */
74
96
  export type BuildBlockInCheckpointResult = {
75
97
  block: L2Block;
76
- publicGas: Gas;
77
98
  publicProcessorDuration: number;
78
99
  numTxs: number;
79
100
  failedTxs: FailedTx[];
80
101
  usedTxs: Tx[];
81
- usedTxBlobFields: number;
82
102
  };
83
103
 
84
104
  /** Interface for building blocks within a checkpoint context. */
@@ -13,6 +13,8 @@ export interface SequencerConfig {
13
13
  sequencerPollingIntervalMS?: number;
14
14
  /** The maximum number of txs to include in a block. */
15
15
  maxTxsPerBlock?: number;
16
+ /** The maximum number of txs across all blocks in a checkpoint. */
17
+ maxTxsPerCheckpoint?: number;
16
18
  /** The minimum number of txs to include in a block. */
17
19
  minTxsPerBlock?: number;
18
20
  /** The minimum number of valid txs (after execution) to include in a block. If not set, falls back to minTxsPerBlock. */
@@ -23,6 +25,8 @@ export interface SequencerConfig {
23
25
  maxL2BlockGas?: number;
24
26
  /** The maximum DA block gas. */
25
27
  maxDABlockGas?: number;
28
+ /** Per-block gas budget multiplier for both L2 and DA gas. Budget = (checkpointLimit / maxBlocks) * multiplier. */
29
+ gasPerBlockAllocationMultiplier?: number;
26
30
  /** Recipient of block reward. */
27
31
  coinbase?: EthAddress;
28
32
  /** Address to receive fees. */
@@ -33,8 +37,6 @@ export interface SequencerConfig {
33
37
  acvmBinaryPath?: string;
34
38
  /** The list of functions calls allowed to run in setup */
35
39
  txPublicSetupAllowList?: AllowedElement[];
36
- /** Max block size */
37
- maxBlockSizeInBytes?: number;
38
40
  /** Payload address to vote for */
39
41
  governanceProposerPayload?: EthAddress;
40
42
  /** Whether to enforce the time table when building blocks */
@@ -85,17 +87,18 @@ export const SequencerConfigSchema = zodFor<SequencerConfig>()(
85
87
  z.object({
86
88
  sequencerPollingIntervalMS: z.number().optional(),
87
89
  maxTxsPerBlock: z.number().optional(),
90
+ maxTxsPerCheckpoint: z.number().optional(),
88
91
  minValidTxsPerBlock: z.number().optional(),
89
92
  minTxsPerBlock: z.number().optional(),
90
93
  maxL2BlockGas: z.number().optional(),
91
94
  publishTxsWithProposals: z.boolean().optional(),
92
95
  maxDABlockGas: z.number().optional(),
96
+ gasPerBlockAllocationMultiplier: z.number().optional(),
93
97
  coinbase: schemas.EthAddress.optional(),
94
98
  feeRecipient: schemas.AztecAddress.optional(),
95
99
  acvmWorkingDirectory: z.string().optional(),
96
100
  acvmBinaryPath: z.string().optional(),
97
101
  txPublicSetupAllowList: z.array(AllowedElementSchema).optional(),
98
- maxBlockSizeInBytes: z.number().optional(),
99
102
  governanceProposerPayload: schemas.EthAddress.optional(),
100
103
  l1PublishingTime: z.number().optional(),
101
104
  enforceTimeTable: z.boolean().optional(),
@@ -134,7 +137,12 @@ type SequencerConfigOptionalKeys =
134
137
  | 'l1PublishingTime'
135
138
  | 'txPublicSetupAllowList'
136
139
  | 'minValidTxsPerBlock'
137
- | 'minBlocksForCheckpoint';
140
+ | 'minBlocksForCheckpoint'
141
+ | 'maxTxsPerBlock'
142
+ | 'maxTxsPerCheckpoint'
143
+ | 'maxL2BlockGas'
144
+ | 'maxDABlockGas'
145
+ | 'gasPerBlockAllocationMultiplier';
138
146
 
139
147
  export type ResolvedSequencerConfig = Prettify<
140
148
  Required<Omit<SequencerConfig, SequencerConfigOptionalKeys>> & Pick<SequencerConfig, SequencerConfigOptionalKeys>
@@ -234,6 +234,15 @@ export class PrivateKernelTailCircuitPublicInputs {
234
234
  return noteHashes.filter(n => !n.isZero());
235
235
  }
236
236
 
237
+ getNonEmptyL2ToL1Msgs() {
238
+ const l2ToL1Msgs = this.forPublic
239
+ ? this.forPublic.nonRevertibleAccumulatedData.l2ToL1Msgs.concat(
240
+ this.forPublic.revertibleAccumulatedData.l2ToL1Msgs,
241
+ )
242
+ : this.forRollup!.end.l2ToL1Msgs;
243
+ return l2ToL1Msgs.filter(m => !m.isEmpty());
244
+ }
245
+
237
246
  getNonEmptyNullifiers() {
238
247
  const nullifiers = this.forPublic
239
248
  ? this.forPublic.nonRevertibleAccumulatedData.nullifiers.concat(
@@ -98,6 +98,7 @@ export const mockTx = async (
98
98
  publicCalldataSize = 2,
99
99
  feePayer,
100
100
  chonkProof = ChonkProof.random(),
101
+ gasLimits,
101
102
  maxFeesPerGas = new GasFees(10, 10),
102
103
  maxPriorityFeesPerGas,
103
104
  gasUsed = Gas.empty(),
@@ -114,6 +115,7 @@ export const mockTx = async (
114
115
  publicCalldataSize?: number;
115
116
  feePayer?: AztecAddress;
116
117
  chonkProof?: ChonkProof;
118
+ gasLimits?: Gas;
117
119
  maxFeesPerGas?: GasFees;
118
120
  maxPriorityFeesPerGas?: GasFees;
119
121
  gasUsed?: Gas;
@@ -132,7 +134,7 @@ export const mockTx = async (
132
134
  const data = PrivateKernelTailCircuitPublicInputs.empty();
133
135
  const firstNullifier = new Nullifier(new Fr(seed + 1), Fr.ZERO, 0);
134
136
  data.constants.anchorBlockHeader = anchorBlockHeader;
135
- data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas, maxPriorityFeesPerGas });
137
+ data.constants.txContext.gasSettings = GasSettings.default({ gasLimits, maxFeesPerGas, maxPriorityFeesPerGas });
136
138
  data.feePayer = feePayer ?? (await AztecAddress.random());
137
139
  data.gasUsed = gasUsed;
138
140
  data.constants.txContext.chainId = chainId;
@@ -425,10 +427,13 @@ export async function mockCheckpointAndMessages(
425
427
  Partial<Parameters<typeof L2Block.random>[1]> = {},
426
428
  ) {
427
429
  const slotNumber = options.slotNumber ?? SlotNumber(Number(checkpointNumber) * 10);
430
+ const globals = GlobalVariables.random({ slotNumber, ...options });
428
431
  const blocksAndMessages = [];
432
+
429
433
  // Track the previous block's archive to ensure consecutive blocks have consistent archive roots.
430
434
  // The current block's header.lastArchive must equal the previous block's archive.
431
435
  let lastArchive: AppendOnlyTreeSnapshot | undefined = previousArchive;
436
+
432
437
  // Pass maxEffects via txOptions so it reaches TxEffect.random
433
438
  const txOptions = maxEffects !== undefined ? { maxEffects } : {};
434
439
  for (let i = 0; i < (blocks?.length ?? numBlocks); i++) {
@@ -437,11 +442,11 @@ export async function mockCheckpointAndMessages(
437
442
  block:
438
443
  blocks?.[i] ??
439
444
  (await L2Block.random(blockNumber, {
445
+ ...globals,
440
446
  checkpointNumber,
441
447
  indexWithinCheckpoint: IndexWithinCheckpoint(i),
442
448
  txsPerBlock: numTxsPerBlock,
443
449
  txOptions,
444
- slotNumber,
445
450
  ...options,
446
451
  ...makeBlockOptions(blockNumber),
447
452
  ...(lastArchive ? { lastArchive } : {}),
@@ -455,12 +460,18 @@ export async function mockCheckpointAndMessages(
455
460
 
456
461
  const messages = blocksAndMessages[0].messages;
457
462
  const inHash = computeInHashFromL1ToL2Messages(messages);
458
- const checkpoint = await Checkpoint.random(checkpointNumber, { numBlocks: 0, slotNumber, inHash, ...options });
463
+ const firstBlockLastArchive = blocksAndMessages[0].block.header.lastArchive;
464
+ const checkpoint = await Checkpoint.random(checkpointNumber, {
465
+ numBlocks: 0,
466
+ inHash,
467
+ ...options,
468
+ ...globals,
469
+ lastArchive: firstBlockLastArchive,
470
+ lastArchiveRoot: firstBlockLastArchive.root,
471
+ archive: lastArchive,
472
+ });
473
+
459
474
  checkpoint.blocks = blocksAndMessages.map(({ block }) => block);
460
- // Set the checkpoint's archive to match the last block's archive for proper chaining.
461
- // When the archiver reconstructs checkpoints from L1, it uses the checkpoint's archive root
462
- // from the L1 event to set the last block's archive. Without this, the archive chain breaks.
463
- checkpoint.archive = lastArchive!;
464
475
 
465
476
  // Return lastArchive so callers can chain it across multiple checkpoints
466
477
  return { checkpoint, messages, lastArchive };
@@ -176,6 +176,12 @@ export class BlockHeader {
176
176
  this._cachedHash = Promise.resolve(new BlockHash(hashed));
177
177
  }
178
178
 
179
+ /** Recomputes the cached hash. Used for testing when header fields are mutated via unfreeze. */
180
+ recomputeHash(): Promise<BlockHash> {
181
+ this._cachedHash = undefined;
182
+ return this.hash();
183
+ }
184
+
179
185
  static random(overrides: Partial<FieldsOf<BlockHeader>> & Partial<FieldsOf<GlobalVariables>> = {}): BlockHeader {
180
186
  return BlockHeader.from({
181
187
  lastArchive: AppendOnlyTreeSnapshot.random(),
package/src/tx/tx.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { DA_GAS_PER_FIELD, TX_DA_GAS_OVERHEAD } from '@aztec/constants';
1
2
  import { Buffer32 } from '@aztec/foundation/buffer';
2
3
  import { Fr } from '@aztec/foundation/curves/bn254';
3
4
  import type { ZodFor } from '@aztec/foundation/schemas';
4
5
  import { BufferReader, serializeArrayOfBufferableToVector, serializeToBuffer } from '@aztec/foundation/serialize';
5
- import type { FieldsOf } from '@aztec/foundation/types';
6
+ import { type FieldsOf, unfreeze } from '@aztec/foundation/types';
6
7
 
7
8
  import { z } from 'zod';
8
9
 
@@ -264,16 +265,24 @@ export class Tx extends Gossipable {
264
265
  }
265
266
 
266
267
  /**
267
- * Estimates the tx size based on its private effects. Note that the actual size of the tx
268
- * after processing will probably be larger, as public execution would generate more data.
268
+ * Returns the number of fields this tx's effects will occupy in the blob,
269
+ * based on its private side effects only. Accurate for txs without public calls.
270
+ * For txs with public calls, the actual size will be larger due to public execution outputs.
269
271
  */
270
- getEstimatedPrivateTxEffectsSize() {
271
- return (
272
- this.data.getNonEmptyNoteHashes().length * Fr.SIZE_IN_BYTES +
273
- this.data.getNonEmptyNullifiers().length * Fr.SIZE_IN_BYTES +
274
- this.data.getEmittedPrivateLogsLength() * Fr.SIZE_IN_BYTES +
275
- this.data.getEmittedContractClassLogsLength() * Fr.SIZE_IN_BYTES
276
- );
272
+ getPrivateTxEffectsSizeInFields(): number {
273
+ // 3 fields overhead: tx_start_marker, tx_hash, tx_fee.
274
+ // TX_DA_GAS_OVERHEAD is defined as N * DA_GAS_PER_FIELD, so this division is always exact.
275
+ const overheadFields = TX_DA_GAS_OVERHEAD / DA_GAS_PER_FIELD;
276
+ const noteHashes = this.data.getNonEmptyNoteHashes().length;
277
+ const nullifiers = this.data.getNonEmptyNullifiers().length;
278
+ const l2ToL1Msgs = this.data.getNonEmptyL2ToL1Msgs().length;
279
+ // Each private log occupies (emittedLength + 1) fields: content + length field
280
+ const privateLogFields = this.data.getNonEmptyPrivateLogs().reduce((acc, log) => acc + log.emittedLength + 1, 0);
281
+ // Each contract class log occupies (length + 1) fields: content + contract address
282
+ const contractClassLogFields = this.data
283
+ .getNonEmptyContractClassLogsHashes()
284
+ .reduce((acc, log) => acc + log.logHash.length + 1, 0);
285
+ return overheadFields + noteHashes + nullifiers + l2ToL1Msgs + privateLogFields + contractClassLogFields;
277
286
  }
278
287
 
279
288
  /**
@@ -309,7 +318,7 @@ export class Tx extends Gossipable {
309
318
 
310
319
  /** Recomputes the tx hash. Used for testing purposes only when a property of the tx was mutated. */
311
320
  public async recomputeHash(): Promise<TxHash> {
312
- (this as any).txHash = await Tx.computeTxHash(this);
321
+ unfreeze(this).txHash = await Tx.computeTxHash(this);
313
322
  return this.txHash;
314
323
  }
315
324