Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export type EnvVar =
| 'SEQ_MAX_DA_BLOCK_GAS'
| 'SEQ_MAX_L2_BLOCK_GAS'
| 'SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER'
| 'SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET'
| 'SEQ_PUBLISHER_PRIVATE_KEY'
| 'SEQ_PUBLISHER_PRIVATE_KEYS'
| 'SEQ_PUBLISHER_ADDRESSES'
Expand Down
28 changes: 17 additions & 11 deletions yarn-project/sequencer-client/src/client/sequencer-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ describe('computeBlockLimits', () => {
describe('L2 gas', () => {
it('derives maxL2BlockGas from rollupManaLimit when not explicitly set', () => {
const rollupManaLimit = 1_000_000;
// Single block mode (maxNumberOfBlocks=1), default multiplier=2:
// min(1_000_000, ceil(1_000_000 / 1 * 2)) = min(1_000_000, 2_000_000) = 1_000_000
// Single block mode (maxNumberOfBlocks=1), default multiplier=1.2:
// min(1_000_000, ceil(1_000_000 / 1 * 1.2)) = min(1_000_000, 1_200_000) = 1_000_000
const result = computeBlockLimits(makeConfig(), rollupManaLimit, 12, log);
expect(result.maxL2BlockGas).toBe(rollupManaLimit);
});
Expand All @@ -43,8 +43,8 @@ describe('computeBlockLimits', () => {
const daLimit = MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT;

it('derives maxDABlockGas from DA checkpoint limit when not explicitly set', () => {
// Single block mode (maxNumberOfBlocks=1), default multiplier=2:
// min(daLimit, ceil(daLimit / 1 * 2)) = min(daLimit, daLimit * 2) = daLimit
// Single block mode (maxNumberOfBlocks=1), default multiplier=1.2:
// min(daLimit, ceil(daLimit / 1 * 1.2)) = min(daLimit, daLimit * 1.2) = daLimit
const result = computeBlockLimits(makeConfig(), 1_000_000, 12, log);
expect(result.maxDABlockGas).toBe(daLimit);
});
Expand Down Expand Up @@ -78,14 +78,14 @@ describe('computeBlockLimits', () => {
});

it('derives maxTxsPerBlock from maxTxsPerCheckpoint when per-block not set', () => {
// Multi-block mode with maxNumberOfBlocks=5, multiplier=2:
// min(100, ceil(100 / 5 * 2)) = min(100, 40) = 40
// Multi-block mode with maxNumberOfBlocks=5, multiplier=1.2:
// min(100, ceil(100 / 5 * 1.2)) = min(100, 24) = 24
const config = makeConfig({
maxTxsPerCheckpoint: 100,
blockDurationMs: 8000,
});
const result = computeBlockLimits(config, 1_000_000, 12, log);
expect(result.maxTxsPerBlock).toBe(40);
expect(result.maxTxsPerBlock).toBe(24);
});
});

Expand All @@ -97,14 +97,20 @@ describe('computeBlockLimits', () => {
// timeReservedAtEnd = 8 + 19 = 27
// timeAvailableForBlocks = 72 - 1 - 27 = 44
// maxNumberOfBlocks = floor(44 / 8) = 5
// With multiplier=2 and rollupManaLimit=1_000_000:
// maxL2BlockGas = min(1_000_000, ceil(1_000_000 / 5 * 2)) = min(1_000_000, 400_000) = 400_000
// With multiplier=1.2 and rollupManaLimit=1_000_000:
// maxL2BlockGas = min(1_000_000, ceil(1_000_000 / 5 * 1.2)) = min(1_000_000, 240_000) = 240_000
const config = makeConfig({ blockDurationMs: 8000 });
const result = computeBlockLimits(config, 1_000_000, 12, log);
expect(result.maxL2BlockGas).toBe(400_000);
expect(result.maxL2BlockGas).toBe(240_000);

const daLimit = MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT;
expect(result.maxDABlockGas).toBe(Math.min(daLimit, Math.ceil((daLimit / 5) * 2)));
expect(result.maxDABlockGas).toBe(Math.min(daLimit, Math.ceil((daLimit / 5) * 1.2)));
});

it('returns maxBlocksPerCheckpoint from timetable', () => {
const config = makeConfig({ blockDurationMs: 8000 });
const result = computeBlockLimits(config, 1_000_000, 12, log);
expect(result.maxBlocksPerCheckpoint).toBe(5);
});
});
});
8 changes: 4 additions & 4 deletions yarn-project/sequencer-client/src/client/sequencer-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class SequencerClient {
const l1PublishingTimeBasedOnChain = isAnvilTestChain(config.l1ChainId) ? 1 : ethereumSlotDuration;
const l1PublishingTime = config.l1PublishingTime ?? l1PublishingTimeBasedOnChain;

const { maxL2BlockGas, maxDABlockGas, maxTxsPerBlock } = computeBlockLimits(
const { maxL2BlockGas, maxDABlockGas, maxTxsPerBlock, maxBlocksPerCheckpoint } = computeBlockLimits(
config,
rollupManaLimit,
l1PublishingTime,
Expand All @@ -183,7 +183,7 @@ export class SequencerClient {
deps.dateProvider,
epochCache,
rollupContract,
{ ...config, l1PublishingTime, maxL2BlockGas, maxDABlockGas, maxTxsPerBlock },
{ ...config, l1PublishingTime, maxL2BlockGas, maxDABlockGas, maxTxsPerBlock, maxBlocksPerCheckpoint },
telemetryClient,
log,
);
Expand Down Expand Up @@ -257,7 +257,7 @@ export function computeBlockLimits(
rollupManaLimit: number,
l1PublishingTime: number,
log: ReturnType<typeof createLogger>,
): { maxL2BlockGas: number; maxDABlockGas: number; maxTxsPerBlock: number } {
): { maxL2BlockGas: number; maxDABlockGas: number; maxTxsPerBlock: number; maxBlocksPerCheckpoint: number } {
const maxNumberOfBlocks = new SequencerTimetable({
ethereumSlotDuration: config.ethereumSlotDuration,
aztecSlotDuration: config.aztecSlotDuration,
Expand Down Expand Up @@ -331,5 +331,5 @@ export function computeBlockLimits(
multiplier,
});

return { maxL2BlockGas, maxDABlockGas, maxTxsPerBlock };
return { maxL2BlockGas, maxDABlockGas, maxTxsPerBlock, maxBlocksPerCheckpoint: maxNumberOfBlocks };
}
12 changes: 11 additions & 1 deletion yarn-project/sequencer-client/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export const DefaultSequencerConfig = {
minTxsPerBlock: 1,
buildCheckpointIfEmpty: false,
publishTxsWithProposals: false,
perBlockAllocationMultiplier: 2,
perBlockAllocationMultiplier: 1.2,
redistributeCheckpointBudget: true,
enforceTimeTable: true,
attestationPropagationTime: DEFAULT_P2P_PROPAGATION_TIME,
secondsBeforeInvalidatingBlockAsCommitteeMember: 144, // 12 L1 blocks
Expand Down Expand Up @@ -112,6 +113,15 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
' Values greater than one allow early blocks to use more than their even share, relying on checkpoint-level capping for later blocks.',
...numberConfigHelper(DefaultSequencerConfig.perBlockAllocationMultiplier),
},
redistributeCheckpointBudget: {
env: 'SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET',
description:
'Redistribute remaining checkpoint budget evenly across remaining blocks instead of allowing a single block to consume the entire remaining budget.',
...booleanConfigHelper(DefaultSequencerConfig.redistributeCheckpointBudget),
},
maxBlocksPerCheckpoint: {
description: 'Computed max number of blocks per checkpoint from timetable.',
},
coinbase: {
env: 'COINBASE',
parseEnv: (val: string) => (val ? EthAddress.fromString(val) : undefined),
Expand Down
6 changes: 6 additions & 0 deletions yarn-project/stdlib/src/interfaces/block-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export type FullNodeBlockBuilderConfig = Pick<L1RollupConstants, 'l1GenesisTime'
| 'maxTxsPerCheckpoint'
| 'maxL2BlockGas'
| 'maxDABlockGas'
| 'redistributeCheckpointBudget'
| 'perBlockAllocationMultiplier'
| 'maxBlocksPerCheckpoint'
>;

export const FullNodeBlockBuilderConfigKeys: (keyof FullNodeBlockBuilderConfig)[] = [
Expand All @@ -79,6 +82,9 @@ export const FullNodeBlockBuilderConfigKeys: (keyof FullNodeBlockBuilderConfig)[
'maxL2BlockGas',
'maxDABlockGas',
'rollupManaLimit',
'redistributeCheckpointBudget',
'perBlockAllocationMultiplier',
'maxBlocksPerCheckpoint',
] as const;

/** Thrown when no valid transactions are available to include in a block after processing, and this is not the first block in a checkpoint. */
Expand Down
10 changes: 9 additions & 1 deletion yarn-project/stdlib/src/interfaces/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export interface SequencerConfig {
maxDABlockGas?: number;
/** Per-block gas budget multiplier for both L2 and DA gas. Budget = (checkpointLimit / maxBlocks) * multiplier. */
perBlockAllocationMultiplier?: number;
/** Redistribute remaining checkpoint budget evenly across remaining blocks instead of allowing a single block to consume the entire remaining budget. */
redistributeCheckpointBudget?: boolean;
/** Computed max number of blocks per checkpoint from timetable. */
maxBlocksPerCheckpoint?: number;
/** Recipient of block reward. */
coinbase?: EthAddress;
/** Address to receive fees. */
Expand Down Expand Up @@ -94,6 +98,8 @@ export const SequencerConfigSchema = zodFor<SequencerConfig>()(
publishTxsWithProposals: z.boolean().optional(),
maxDABlockGas: z.number().optional(),
perBlockAllocationMultiplier: z.number().optional(),
redistributeCheckpointBudget: z.boolean().optional(),
maxBlocksPerCheckpoint: z.number().optional(),
coinbase: schemas.EthAddress.optional(),
feeRecipient: schemas.AztecAddress.optional(),
acvmWorkingDirectory: z.string().optional(),
Expand Down Expand Up @@ -142,7 +148,9 @@ type SequencerConfigOptionalKeys =
| 'maxTxsPerCheckpoint'
| 'maxL2BlockGas'
| 'maxDABlockGas'
| 'perBlockAllocationMultiplier';
| 'perBlockAllocationMultiplier'
| 'redistributeCheckpointBudget'
| 'maxBlocksPerCheckpoint';

export type ResolvedSequencerConfig = Prettify<
Required<Omit<SequencerConfig, SequencerConfigOptionalKeys>> & Pick<SequencerConfig, SequencerConfigOptionalKeys>
Expand Down
7 changes: 4 additions & 3 deletions yarn-project/validator-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,11 @@ L1 enforces gas and blob capacity per checkpoint. The node enforces these during

Per-block budgets prevent one block from consuming the entire checkpoint budget.

**Proposer**: `computeBlockLimits()` derives budgets at startup as `min(checkpointLimit, ceil(checkpointLimit / maxBlocks * multiplier))`, where `maxBlocks` comes from the timetable and `multiplier` defaults to 2. The multiplier greater than 1 allows early blocks to use more than their even share of the checkpoint budget, since different blocks hit different limit dimensions (L2 gas, DA gas, blob fields) — a strict even split would waste capacity. Operators can override via `SEQ_MAX_L2_BLOCK_GAS` / `SEQ_MAX_DA_BLOCK_GAS` / `SEQ_MAX_TX_PER_BLOCK` (capped at checkpoint limits). Per-block TX limits follow the same derivation pattern when `SEQ_MAX_TX_PER_CHECKPOINT` is set.
**Proposer**: `computeBlockLimits()` derives budgets at startup as `min(checkpointLimit, ceil(checkpointLimit / maxBlocks * multiplier))`, where `maxBlocks` comes from the timetable and `multiplier` defaults to 1.2. The multiplier greater than 1 allows early blocks to use more than their even share of the checkpoint budget, since different blocks hit different limit dimensions (L2 gas, DA gas, blob fields) — a strict even split would waste capacity. Operators can override via `SEQ_MAX_L2_BLOCK_GAS` / `SEQ_MAX_DA_BLOCK_GAS` / `SEQ_MAX_TX_PER_BLOCK` (capped at checkpoint limits). Per-block TX limits follow the same derivation pattern when `SEQ_MAX_TX_PER_CHECKPOINT` is set.

**Validator**: Optionally enforces per-block limits via `VALIDATOR_MAX_L2_BLOCK_GAS`, `VALIDATOR_MAX_DA_BLOCK_GAS`, and `VALIDATOR_MAX_TX_PER_BLOCK`. When set, these are passed to `buildBlock` during re-execution and to `validateCheckpoint` for final validation. When unset, no per-block limit is enforced for that dimension (checkpoint-level protocol limits still apply). These are independent of the `SEQ_` vars so operators can tune proposer and validation limits separately.

**Checkpoint-level capping**: `CheckpointBuilder.capLimitsByCheckpointBudgets()` always runs before tx processing, capping per-block limits by `checkpointBudget - sum(used by prior blocks)` for all three gas dimensions and for transaction count (when `SEQ_MAX_TX_PER_CHECKPOINT` is set). This applies to both proposer and validator paths.
**Checkpoint-level capping**: `CheckpointBuilder.capLimitsByCheckpointBudgets()` always runs before tx processing, capping per-block limits by the remaining checkpoint budget. When `SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET` is enabled (default: true), the remaining budget is distributed evenly across remaining blocks with the multiplier applied: `min(perBlockLimit, ceil(remainingBudget / remainingBlocks * multiplier), remainingBudget)`. This prevents early blocks from consuming the entire checkpoint budget, producing smoother distribution. When disabled, each block can consume up to the full remaining budget, ie caps by `checkpointBudget - sum(used by prior blocks)`. This applies to all four dimensions (L2 gas, DA gas, blob fields, transaction count). Validators always cap by the total remaining.

### Per-transaction enforcement

Expand All @@ -259,7 +259,8 @@ Per-block budgets prevent one block from consuming the entire checkpoint budget.
| `SEQ_MAX_DA_BLOCK_GAS` | *auto* | Per-block DA gas. Auto-derived from checkpoint DA limit / maxBlocks * multiplier. |
| `SEQ_MAX_TX_PER_BLOCK` | *none* | Per-block tx count. If `SEQ_MAX_TX_PER_CHECKPOINT` is set and per-block is not, derived as `ceil(checkpointLimit / maxBlocks * multiplier)`. |
| `SEQ_MAX_TX_PER_CHECKPOINT` | *none* | Total txs across all blocks in a checkpoint. When set, per-block tx limit is derived from it (unless explicitly overridden) and checkpoint-level capping is enforced. |
| `SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER` | 2 | Multiplier for per-block budget computation. |
| `SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER` | 1.2 | Multiplier for per-block budget computation. |
| `SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET` | true | Redistribute remaining checkpoint budget evenly across remaining blocks instead of allowing one block to consume it all. |
| `VALIDATOR_MAX_L2_BLOCK_GAS` | *none* | Per-block L2 gas limit for validation. Proposals exceeding this are rejected. |
| `VALIDATOR_MAX_DA_BLOCK_GAS` | *none* | Per-block DA gas limit for validation. Proposals exceeding this are rejected. |
| `VALIDATOR_MAX_TX_PER_BLOCK` | *none* | Per-block tx count limit for validation. Proposals exceeding this are rejected. |
Expand Down
124 changes: 124 additions & 0 deletions yarn-project/validator-client/src/checkpoint_builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ describe('CheckpointBuilder', () => {
l1ChainId: 1,
rollupVersion: 1,
rollupManaLimit: 200_000_000,
redistributeCheckpointBudget: false,
...overrideConfig,
};

Expand Down Expand Up @@ -416,4 +417,127 @@ describe('CheckpointBuilder', () => {
expect(capped.maxTransactions).toBeUndefined();
});
});

describe('redistributeCheckpointBudget', () => {
it('evenly splits budget with multiplier=1', () => {
const rollupManaLimit = 1_000_000;
setupBuilder({
redistributeCheckpointBudget: true,
perBlockAllocationMultiplier: 1,
maxBlocksPerCheckpoint: 5,
rollupManaLimit,
});

lightweightCheckpointBuilder.getBlocks.mockReturnValue([]);

const opts: PublicProcessorLimits = {};
const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits(opts);

// Fair share = ceil(1_000_000 / 5 * 1) = 200_000
expect(capped.maxBlockGas!.l2Gas).toBe(200_000);
});

it('computes fair share with multiplier=1.2, 5 max blocks, 2 existing', () => {
const rollupManaLimit = 1_000_000;
setupBuilder({
redistributeCheckpointBudget: true,
perBlockAllocationMultiplier: 1.2,
maxBlocksPerCheckpoint: 5,
rollupManaLimit,
});

// 2 existing blocks used 400_000 mana total
lightweightCheckpointBuilder.getBlocks.mockReturnValue([
createMockBlock({ manaUsed: 200_000, txBlobFields: [10], blockBlobFieldCount: 20 }),
createMockBlock({ manaUsed: 200_000, txBlobFields: [10], blockBlobFieldCount: 20 }),
]);

const opts: PublicProcessorLimits = {};
const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits(opts);

// remainingMana = 600_000, remainingBlocks = 3, multiplier = 1.2
// fairShare = ceil(600_000 / 3 * 1.2) = ceil(240_000) = 240_000
expect(capped.maxBlockGas!.l2Gas).toBe(240_000);
});

it('gives all remaining budget to last block (remainingBlocks=1)', () => {
const rollupManaLimit = 1_000_000;
setupBuilder({
redistributeCheckpointBudget: true,
perBlockAllocationMultiplier: 1.2,
maxBlocksPerCheckpoint: 3,
rollupManaLimit,
});

// 2 existing blocks used 800_000 total
lightweightCheckpointBuilder.getBlocks.mockReturnValue([
createMockBlock({ manaUsed: 400_000, txBlobFields: [10], blockBlobFieldCount: 20 }),
createMockBlock({ manaUsed: 400_000, txBlobFields: [10], blockBlobFieldCount: 20 }),
]);

const opts: PublicProcessorLimits = {};
const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits(opts);

// remainingMana = 200_000, remainingBlocks = 1, multiplier = 1.2
// fairShare = ceil(200_000 / 1 * 1.2) = 240_000. min(200_000, 240_000, 200_000) = 200_000
expect(capped.maxBlockGas!.l2Gas).toBe(200_000);
});

it('uses old behavior when redistributeCheckpointBudget is false', () => {
const rollupManaLimit = 1_000_000;
setupBuilder({
redistributeCheckpointBudget: false,
maxBlocksPerCheckpoint: 5,
rollupManaLimit,
});

lightweightCheckpointBuilder.getBlocks.mockReturnValue([
createMockBlock({ manaUsed: 200_000, txBlobFields: [10], blockBlobFieldCount: 20 }),
]);

const opts: PublicProcessorLimits = {};
const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits(opts);

// Old behavior: no fair share, just remaining budget = 800_000
expect(capped.maxBlockGas!.l2Gas).toBe(800_000);
});

it('redistributes DA gas across remaining blocks', () => {
setupBuilder({
redistributeCheckpointBudget: true,
perBlockAllocationMultiplier: 1,
maxBlocksPerCheckpoint: 4,
});

lightweightCheckpointBuilder.getBlocks.mockReturnValue([]);

const opts: PublicProcessorLimits = {};
const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits(opts);

// fairShareDA = ceil(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT / 4 * 1)
const expectedDA = Math.ceil(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT / 4);
expect(capped.maxBlockGas!.daGas).toBe(expectedDA);
});

it('redistributes tx count across remaining blocks', () => {
setupBuilder({
redistributeCheckpointBudget: true,
perBlockAllocationMultiplier: 1,
maxBlocksPerCheckpoint: 4,
maxTxsPerCheckpoint: 100,
});

// 1 existing block with 10 txs
lightweightCheckpointBuilder.getBlocks.mockReturnValue([
createMockBlock({ manaUsed: 0, txBlobFields: new Array(10).fill(1), blockBlobFieldCount: 20 }),
]);

const opts: PublicProcessorLimits = {};
const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits(opts);

// remainingTxs = 90, remainingBlocks = 3, multiplier = 1
// fairShareTxs = ceil(90 / 3 * 1) = 30
expect(capped.maxTransactions).toBe(30);
});
});
});
Loading
Loading