Skip to content

Commit ab636e8

Browse files
authored
chore: merge staging to v4-next (#22463)
Triggering this manually to get a few things in before 4.2.0 cutoff
2 parents bc4993a + 3469c39 commit ab636e8

17 files changed

Lines changed: 353 additions & 65 deletions

File tree

docs/docs-operate/operators/reference/cli-reference.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,7 @@ tags:
4949
LOCAL NETWORK
5050

5151
--local-network
52-
Starts Aztec local network
53-
54-
--local-network.noPXE ($NO_PXE)
55-
Do not expose PXE service on local network start
52+
Starts Aztec local network (also exposes the AztecNodeDebug endpoints on the node - just like if --node-debug was set)
5653

5754
--local-network.l1Mnemonic <value> (default: test test test test test test test test test test test junk)($MNEMONIC)
5855
Mnemonic for L1 accounts. Will be used
@@ -65,6 +62,9 @@ tags:
6562
--admin-port <value> (default: 8880) ($AZTEC_ADMIN_PORT)
6663
Port to run admin APIs of Aztec Services on
6764

65+
--node-debug (default: false) ($AZTEC_NODE_DEBUG)
66+
Expose debug endpoints (e.g. mineBlock) on the main RPC port
67+
6868
--api-prefix <value> ($API_PREFIX)
6969
Prefix for API routes on any service that is started
7070

yarn-project/aztec-node/src/aztec-node/server.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
1313
import type { P2P } from '@aztec/p2p';
1414
import { protocolContractsHash } from '@aztec/protocol-contracts';
1515
import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-juice';
16-
import type { GlobalVariableBuilder, SequencerClient } from '@aztec/sequencer-client';
16+
import type { GlobalVariableBuilder, Sequencer, SequencerClient } from '@aztec/sequencer-client';
1717
import type { SlasherClientInterface } from '@aztec/slasher';
1818
import { AztecAddress } from '@aztec/stdlib/aztec-address';
1919
import { BlockHash, type BlockParameter, CheckpointedL2Block, L2Block, type L2BlockSource } from '@aztec/stdlib/block';
@@ -948,6 +948,67 @@ describe('aztec node', () => {
948948
});
949949
});
950950

951+
describe('mineBlock', () => {
952+
const INITIAL_MIN_TXS_PER_BLOCK = 1;
953+
954+
let sequencerClient: MockProxy<SequencerClient>;
955+
let nodeWithSequencer: AztecNodeService;
956+
957+
/** Simulates block number advancing from `from` to `to` after the first call. */
958+
const mockBlockNumberAdvancing = (from: number, to: number) => {
959+
let callCount = 0;
960+
l2BlockSource.getBlockNumber.mockImplementation(() => {
961+
callCount++;
962+
return Promise.resolve(callCount > 1 ? BlockNumber(to) : BlockNumber(from));
963+
});
964+
};
965+
966+
beforeEach(() => {
967+
const sequencer = mock<Sequencer>();
968+
sequencer.getConfig.mockReturnValue({ minTxsPerBlock: INITIAL_MIN_TXS_PER_BLOCK } as any);
969+
970+
sequencerClient = mock<SequencerClient>();
971+
sequencerClient.getSequencer.mockReturnValue(sequencer);
972+
sequencerClient.trigger.mockReturnValue(Promise.resolve());
973+
974+
nodeWithSequencer = new AztecNodeService(
975+
nodeConfig,
976+
p2p,
977+
l2BlockSource,
978+
mock(),
979+
mock(),
980+
mock(),
981+
worldState,
982+
sequencerClient,
983+
undefined,
984+
undefined,
985+
undefined,
986+
undefined,
987+
12345,
988+
rollupVersion.toNumber(),
989+
globalVariablesBuilder,
990+
epochCache,
991+
getPackageVersion() ?? '',
992+
new TestCircuitVerifier(),
993+
);
994+
});
995+
996+
it('throws when no sequencer is running', async () => {
997+
await expect(node.mineBlock()).rejects.toThrow('Cannot mine block: no sequencer is running');
998+
});
999+
1000+
it('restores minTxsPerBlock after successful block production', async () => {
1001+
mockBlockNumberAdvancing(5, 6);
1002+
1003+
await nodeWithSequencer.mineBlock();
1004+
1005+
const updateCalls = sequencerClient.updateConfig.mock.calls;
1006+
expect(updateCalls[0][0]).toEqual({ minTxsPerBlock: 0 });
1007+
// Last call to update calls should revert the value to the original
1008+
expect(updateCalls[1][0]).toEqual({ minTxsPerBlock: INITIAL_MIN_TXS_PER_BLOCK });
1009+
});
1010+
});
1011+
9511012
describe('getL2ToL1Messages', () => {
9521013
const makeCheckpointedBlock = (slotNumber: number, l2ToL1MsgsByTx: Fr[][]): CheckpointedL2Block => {
9531014
const block = L2Block.empty(

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
1414
import { EthAddress } from '@aztec/foundation/eth-address';
1515
import { BadRequestError } from '@aztec/foundation/json-rpc';
1616
import { type Logger, createLogger } from '@aztec/foundation/log';
17+
import { retryUntil } from '@aztec/foundation/retry';
1718
import { count } from '@aztec/foundation/string';
1819
import { DateProvider, Timer } from '@aztec/foundation/timer';
1920
import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees';
@@ -64,6 +65,7 @@ import {
6465
type AztecNodeAdmin,
6566
type AztecNodeAdminConfig,
6667
AztecNodeAdminConfigSchema,
68+
type AztecNodeDebug,
6769
type GetContractClassLogsResponse,
6870
type GetPublicLogsResponse,
6971
} from '@aztec/stdlib/interfaces/client';
@@ -124,7 +126,7 @@ import { NodeMetrics } from './node_metrics.js';
124126
/**
125127
* The aztec node.
126128
*/
127-
export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
129+
export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDebug, Traceable {
128130
private metrics: NodeMetrics;
129131
private initialHeaderHashPromise: Promise<BlockHash> | undefined = undefined;
130132

@@ -1600,6 +1602,40 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
16001602
this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated');
16011603
}
16021604

1605+
public async mineBlock(): Promise<void> {
1606+
if (!this.sequencer) {
1607+
throw new BadRequestError('Cannot mine block: no sequencer is running');
1608+
}
1609+
1610+
const currentBlockNumber = await this.getBlockNumber();
1611+
1612+
// Use slot duration + 50% buffer as the timeout so this works on running networks too
1613+
const { slotDuration } = await this.blockSource.getL1Constants();
1614+
const timeoutSeconds = Math.ceil(slotDuration * 1.5);
1615+
1616+
// Temporarily set minTxsPerBlock to 0 so the sequencer produces a block even with no txs
1617+
const originalMinTxsPerBlock = this.sequencer.getSequencer().getConfig().minTxsPerBlock;
1618+
this.sequencer.updateConfig({ minTxsPerBlock: 0 });
1619+
1620+
try {
1621+
// Trigger the sequencer to produce a block immediately
1622+
void this.sequencer.trigger();
1623+
1624+
// Wait for the new L2 block to appear
1625+
await retryUntil(
1626+
async () => {
1627+
const newBlockNumber = await this.getBlockNumber();
1628+
return newBlockNumber > currentBlockNumber ? true : undefined;
1629+
},
1630+
'mineBlock',
1631+
timeoutSeconds,
1632+
0.1,
1633+
);
1634+
} finally {
1635+
this.sequencer.updateConfig({ minTxsPerBlock: originalMinTxsPerBlock });
1636+
}
1637+
}
1638+
16031639
#getInitialHeaderHash(): Promise<BlockHash> {
16041640
if (!this.initialHeaderHashPromise) {
16051641
this.initialHeaderHashPromise = this.worldStateSynchronizer.getCommitted().getInitialHeader().hash();

yarn-project/aztec/src/cli/aztec_start_action.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '@aztec/foundation/json-rpc/server';
88
import type { LogFn, Logger } from '@aztec/foundation/log';
99
import type { ChainConfig } from '@aztec/stdlib/config';
10-
import { AztecNodeApiSchema } from '@aztec/stdlib/interfaces/client';
10+
import { AztecNodeAdminApiSchema, AztecNodeApiSchema, AztecNodeDebugApiSchema } from '@aztec/stdlib/interfaces/client';
1111
import { getPackageVersion } from '@aztec/stdlib/update-checker';
1212
import { getVersioningMiddleware } from '@aztec/stdlib/versioning';
1313
import { getOtelJsonRpcPropagationMiddleware } from '@aztec/telemetry-client';
@@ -50,6 +50,8 @@ export async function aztecStart(options: any, userLog: LogFn, debugLogger: Logg
5050
// Start Node and PXE JSON-RPC server
5151
signalHandlers.push(stop);
5252
services.node = [node, AztecNodeApiSchema];
53+
adminServices.node = [node, AztecNodeAdminApiSchema];
54+
services.nodeDebug = [node, AztecNodeDebugApiSchema];
5355
} else {
5456
// Route --prover-node through startNode
5557
if (options.proverNode && !options.node) {
@@ -60,6 +62,9 @@ export async function aztecStart(options: any, userLog: LogFn, debugLogger: Logg
6062
const { startNode } = await import('./cmds/start_node.js');
6163
const networkName = getActiveNetworkName(options.network);
6264
({ config } = await startNode(options, signalHandlers, services, adminServices, userLog, networkName));
65+
if (options.nodeDebug && services.node) {
66+
services.nodeDebug = [services.node[0], AztecNodeDebugApiSchema];
67+
}
6368
} else if (options.bot) {
6469
const { startBot } = await import('./cmds/start_bot.js');
6570
await startBot(options, signalHandlers, services, userLog);

yarn-project/aztec/src/cli/aztec_start_options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ export const aztecStartOptions: { [key: string]: AztecStartOption[] } = {
165165
env: 'AZTEC_RESET_ADMIN_API_KEY',
166166
parseVal: val => val === 'true' || val === '1',
167167
},
168+
{
169+
flag: '--node-debug',
170+
description: 'Expose debug endpoints (e.g. mineBlock) on the main RPC port',
171+
defaultValue: false,
172+
env: 'AZTEC_NODE_DEBUG',
173+
parseVal: val => val === undefined || val === 'true' || val === '1',
174+
},
168175
{
169176
flag: '--api-prefix <value>',
170177
description: 'Prefix for API routes on any service that is started',
Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test';
2-
import { BlockNumber } from '@aztec/foundation/branded-types';
3-
import { retryUntil } from '@aztec/foundation/retry';
2+
import { SlotNumber } from '@aztec/foundation/branded-types';
3+
import { createLogger } from '@aztec/foundation/log';
44
import type { DateProvider } from '@aztec/foundation/timer';
5-
import type { SequencerClient } from '@aztec/sequencer-client';
6-
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
5+
import type { AztecNode, AztecNodeDebug } from '@aztec/stdlib/interfaces/client';
76

87
/**
98
* A class that provides utility functions for interacting with the chain.
@@ -12,6 +11,8 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client';
1211
* codes, please consider whether it makes sense to just introduce new utils in your tests instead.
1312
*/
1413
export class CheatCodes {
14+
private logger = createLogger('aztecjs:cheat_codes');
15+
1516
constructor(
1617
/** Cheat codes for L1.*/
1718
public eth: EthCheatCodes,
@@ -30,50 +31,55 @@ export class CheatCodes {
3031

3132
/**
3233
* Warps the L1 timestamp to a target timestamp and mines an L2 block that advances the L2 timestamp to at least
33-
* the target timestamp. L2 timestamp is not advanced exactly to the target timestamp because it is determined
34-
* by the slot number, which advances in fixed intervals.
35-
* This is useful for testing time-dependent contract behavior.
36-
* @param sequencerClient - The sequencer client to use to force an empty block to be mined.
37-
* @param node - The Aztec node used to query if a new block has been mined.
34+
* the target timestamp. If the target timestamp falls within the current L2 slot (which already has a block),
35+
* the timestamp is automatically adjusted forward to the start of the next slot so that `mineBlock()` succeeds.
36+
* @param node - The Aztec node used to force an empty block to be mined.
3837
* @param targetTimestamp - The target timestamp to warp to (in seconds)
3938
*/
40-
async warpL2TimeAtLeastTo(sequencerClient: SequencerClient, node: AztecNode, targetTimestamp: bigint | number) {
41-
const currentL2BlockNumber: BlockNumber = await node.getBlockNumber();
39+
async warpL2TimeAtLeastTo(node: AztecNodeDebug, targetTimestamp: bigint | number) {
40+
const targetBigInt = BigInt(targetTimestamp);
41+
const currentTimestamp = BigInt(await this.eth.lastBlockTimestamp());
4242

43-
// We warp the L1 timestamp
44-
await this.eth.warp(targetTimestamp, { resetBlockInterval: true });
43+
if (targetBigInt <= currentTimestamp) {
44+
throw new Error(
45+
`warpL2TimeAtLeastTo: target timestamp ${targetBigInt} is not in the future (current L1 timestamp is ${currentTimestamp}).`,
46+
);
47+
}
4548

46-
// Wait until an L2 block is mined
47-
const sequencer = sequencerClient.getSequencer();
48-
const minTxsPerBlock = sequencer.getConfig().minTxsPerBlock;
49-
sequencer.updateConfig({ minTxsPerBlock: 0 });
49+
const currentSlot = await this.rollup.getSlot();
50+
const targetSlot = await this.rollup.getSlotAt(targetBigInt);
5051

51-
await retryUntil(
52-
async () => {
53-
const newL2BlockNumber: BlockNumber = await node.getBlockNumber();
54-
return newL2BlockNumber > currentL2BlockNumber;
55-
},
56-
'new block after warping L2 time',
57-
36,
58-
1,
59-
);
52+
let effectiveTimestamp = targetBigInt;
6053

61-
// Restore original minTxsPerBlock
62-
sequencer.updateConfig({ minTxsPerBlock });
54+
if (targetSlot <= currentSlot) {
55+
// Target lands in the same (or earlier) slot — auto-adjust to the next slot's start.
56+
const nextSlot = SlotNumber(currentSlot + 1);
57+
const nextSlotTimestamp = await this.rollup.getTimestampForSlot(nextSlot);
58+
this.logger.warn(
59+
`warpL2TimeAtLeastTo: target timestamp ${targetBigInt} falls in current slot ${currentSlot}. ` +
60+
`Auto-adjusting to start of slot ${nextSlot} at timestamp ${nextSlotTimestamp}.`,
61+
);
62+
effectiveTimestamp = nextSlotTimestamp;
63+
}
64+
65+
await this.eth.warp(effectiveTimestamp, { resetBlockInterval: true });
66+
await node.mineBlock();
6367
}
6468

6569
/**
6670
* Warps the L1 timestamp forward by a specified duration and mines an L2 block that advances the L2 timestamp at
67-
* least by the duration. L2 timestamp is not advanced exactly by the duration because it is determined by the slot
68-
* number, which advances in fixed intervals.
69-
* This is useful for testing time-dependent contract behavior.
70-
* @param sequencerClient - The sequencer client to use to force an empty block to be mined.
71-
* @param node - The Aztec node used to query if a new block has been mined.
71+
* least by the duration. If the duration is too short to cross an L2 slot boundary, the warp is automatically
72+
* extended to the start of the next slot so that `mineBlock()` succeeds.
73+
* @param node - The Aztec node used to force an empty block to be mined.
7274
* @param duration - The duration to advance time by (in seconds)
7375
*/
74-
async warpL2TimeAtLeastBy(sequencerClient: SequencerClient, node: AztecNode, duration: bigint | number) {
76+
async warpL2TimeAtLeastBy(node: AztecNodeDebug, duration: bigint | number) {
77+
if (BigInt(duration) <= 0n) {
78+
throw new Error(`warpL2TimeAtLeastBy: duration must be positive, got ${duration} seconds.`);
79+
}
80+
7581
const currentTimestamp = await this.eth.lastBlockTimestamp();
7682
const targetTimestamp = BigInt(currentTimestamp) + BigInt(duration);
77-
await this.warpL2TimeAtLeastTo(sequencerClient, node, targetTimestamp);
83+
await this.warpL2TimeAtLeastTo(node, targetTimestamp);
7884
}
7985
}

0 commit comments

Comments
 (0)