@aztec/stdlib 5.2.0-nightly.20260813 → 5.2.0-nightly.20260815

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 (48) hide show
  1. package/dest/block/l2_block_source.d.ts +3 -6
  2. package/dest/block/l2_block_source.d.ts.map +1 -1
  3. package/dest/block/l2_block_stream/event_driven_l2_block_stream.d.ts +11 -20
  4. package/dest/block/l2_block_stream/event_driven_l2_block_stream.d.ts.map +1 -1
  5. package/dest/block/l2_block_stream/event_driven_l2_block_stream.js +12 -116
  6. package/dest/checkpoint/simulation_overrides.d.ts +1 -1
  7. package/dest/checkpoint/simulation_overrides.js +2 -2
  8. package/dest/checkpoint/validate.d.ts +9 -1
  9. package/dest/checkpoint/validate.d.ts.map +1 -1
  10. package/dest/checkpoint/validate.js +11 -2
  11. package/dest/config/network-consensus-config.d.ts +1 -1
  12. package/dest/config/network-consensus-config.d.ts.map +1 -1
  13. package/dest/config/network-consensus-config.js +4 -0
  14. package/dest/config/node-rpc-config.d.ts +7 -1
  15. package/dest/config/node-rpc-config.d.ts.map +1 -1
  16. package/dest/config/node-rpc-config.js +17 -1
  17. package/dest/interfaces/aztec-node-admin.d.ts +2 -1
  18. package/dest/interfaces/aztec-node-admin.d.ts.map +1 -1
  19. package/dest/interfaces/aztec-node.d.ts +3 -2
  20. package/dest/interfaces/aztec-node.d.ts.map +1 -1
  21. package/dest/interfaces/aztec-node.js +8 -6
  22. package/dest/interfaces/configs.d.ts +7 -1
  23. package/dest/interfaces/configs.d.ts.map +1 -1
  24. package/dest/interfaces/configs.js +2 -1
  25. package/dest/interfaces/p2p.d.ts +18 -1
  26. package/dest/interfaces/p2p.d.ts.map +1 -1
  27. package/dest/interfaces/p2p.js +8 -0
  28. package/dest/interfaces/validator.d.ts +6 -6
  29. package/dest/interfaces/validator.d.ts.map +1 -1
  30. package/dest/p2p/index.d.ts +2 -1
  31. package/dest/p2p/index.d.ts.map +1 -1
  32. package/dest/p2p/index.js +1 -0
  33. package/dest/p2p/validated_proposal.d.ts +35 -0
  34. package/dest/p2p/validated_proposal.d.ts.map +1 -0
  35. package/dest/p2p/validated_proposal.js +17 -0
  36. package/package.json +8 -8
  37. package/src/block/l2_block_source.ts +2 -5
  38. package/src/block/l2_block_stream/event_driven_l2_block_stream.ts +17 -143
  39. package/src/checkpoint/simulation_overrides.ts +2 -2
  40. package/src/checkpoint/validate.ts +25 -3
  41. package/src/config/network-consensus-config.ts +8 -0
  42. package/src/config/node-rpc-config.ts +27 -1
  43. package/src/interfaces/aztec-node.ts +13 -3
  44. package/src/interfaces/configs.ts +6 -0
  45. package/src/interfaces/p2p.ts +21 -0
  46. package/src/interfaces/validator.ts +6 -4
  47. package/src/p2p/index.ts +1 -0
  48. package/src/p2p/validated_proposal.ts +43 -0
@@ -1,156 +1,51 @@
1
- import { type Logger, createLogger } from '@aztec/foundation/log';
1
+ import { createLogger } from '@aztec/foundation/log';
2
2
  import { RunningPromise } from '@aztec/foundation/running-promise';
3
3
 
4
- import type { BlockData } from '../block_data.js';
5
- import type { L2Block } from '../l2_block.js';
6
4
  import {
7
5
  type ArchiverEmitter,
8
- type BlockQuery,
9
- type BlocksQuery,
10
6
  type L2BlockSource,
11
7
  type L2BlockSourceEventEmitter,
12
8
  L2BlockSourceEvents,
13
- type L2BlockSourceUpdatedEvent,
14
- type L2Tips,
15
9
  } from '../l2_block_source.js';
16
- import { type L2BlockStreamEventHandler, type L2BlockStreamLocalDataProvider, localTipsMatch } from './interfaces.js';
17
- import { L2BlockStream, type L2BlockStreamOptions, type L2BlockStreamSource } from './l2_block_stream.js';
18
-
19
- /** Derives the metadata-only {@link BlockData} view of a hydrated {@link L2Block}. */
20
- async function l2BlockToBlockData(block: L2Block): Promise<BlockData> {
21
- return {
22
- header: block.header,
23
- archive: block.archive,
24
- blockHash: await block.hash(),
25
- checkpointNumber: block.checkpointNumber,
26
- indexWithinCheckpoint: block.indexWithinCheckpoint,
27
- };
28
- }
10
+ import type { L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js';
11
+ import { L2BlockStream, type L2BlockStreamOptions } from './l2_block_stream.js';
29
12
 
30
13
  /** Returns the event emitter of a source that exposes one, or undefined for plain (e.g. RPC-backed) sources. */
31
14
  function getEmitter(source: L2BlockSource | L2BlockSourceEventEmitter): ArchiverEmitter | undefined {
32
15
  return 'events' in source ? source.events : undefined;
33
16
  }
34
17
 
35
- /** Fast-path context for a single sync pass: blocks to serve by number, plus the tips to report as the source's. */
36
- type ActiveUpdate = { byNumber: Map<number, L2Block>; toTips: L2Tips };
37
-
38
- /**
39
- * Wraps a block source so a single sync pass can be served from blocks delivered by an aggregate update event,
40
- * avoiding round-trips to archiver storage. The fast path is only armed (via {@link activate}) for a pass that is
41
- * confirmed caught up to the event's pre-pass tips: in that case the event's `blocksAdded` contiguously cover the
42
- * pass's download range and `toTips` is the exact post-pass tip, so `getL2Tips` can report it without querying the
43
- * source. When the fast path is not armed, every read delegates to the source, so a stale or partial cache never
44
- * changes the sync outcome.
45
- */
46
- class HotBlockSourceAdapter implements L2BlockStreamSource {
47
- /** Set for the duration of one fast-path pass; undefined when reads must delegate to the source. */
48
- private active: ActiveUpdate | undefined;
49
-
50
- constructor(
51
- private readonly source: L2BlockStreamSource,
52
- private readonly log: Logger,
53
- ) {}
54
-
55
- /** Arms the fast path for the current pass: serve these blocks and report `toTips` as the source tips. */
56
- public activate(blocks: readonly L2Block[], toTips: L2Tips): void {
57
- const byNumber = new Map<number, L2Block>();
58
- for (const block of blocks) {
59
- byNumber.set(block.number, block);
60
- }
61
- this.active = { byNumber, toTips };
62
- this.log.trace(`Armed hot-block fast path`, { blocks: byNumber.size, toTips });
63
- }
64
-
65
- /** Disarms the fast path so subsequent reads delegate to the source again. */
66
- public deactivate(): void {
67
- this.active = undefined;
68
- }
69
-
70
- public getL2Tips(): Promise<L2Tips> {
71
- return this.active ? Promise.resolve(this.active.toTips) : this.source.getL2Tips();
72
- }
73
-
74
- public getBlocks(query: BlocksQuery): Promise<L2Block[]> {
75
- const served = this.active ? this.tryServeBlocksFromCache(query) : undefined;
76
- return served ? Promise.resolve(served) : this.source.getBlocks(query);
77
- }
78
-
79
- public getBlockData(query: BlockQuery): Promise<BlockData | undefined> {
80
- if (this.active && 'number' in query) {
81
- const block = this.active.byNumber.get(query.number);
82
- if (block) {
83
- return l2BlockToBlockData(block);
84
- }
85
- }
86
- return this.source.getBlockData(query);
87
- }
88
-
89
- /** Serves a block range from the cache only when it is fully covered by contiguous cached blocks. */
90
- private tryServeBlocksFromCache(query: BlocksQuery): L2Block[] | undefined {
91
- // Only the by-range form is cacheable, and only for the full (not checkpointed-only) chain: the cache may hold
92
- // uncheckpointed blocks that an onlyCheckpointed query must not receive.
93
- if (!this.active || !('from' in query) || query.onlyCheckpointed) {
94
- return undefined;
95
- }
96
- const from = query.from;
97
- const to = from + query.limit - 1;
98
- const blocks: L2Block[] = [];
99
- for (let n = from; n <= to; n++) {
100
- const block = this.active.byNumber.get(n);
101
- if (!block) {
102
- // A gap inside the requested range: bail out and let the source serve the whole range.
103
- return undefined;
104
- }
105
- blocks.push(block);
106
- }
107
- return blocks;
108
- }
109
- }
110
-
111
18
  /**
112
19
  * Event-driven wrapper around {@link L2BlockStream}. Subscribes to the source's aggregate `l2BlockSourceUpdated`
113
- * event (when the source exposes one) to trigger an immediate reconciliation, while keeping the periodic poll as
114
- * the correctness fallback. Subsystems keep consuming the same {@link L2BlockStreamEvent}s; the archiver aggregate
115
- * event is handled entirely here.
116
- *
117
- * The event is passed through to the sync pass via {@link RunningPromise.trigger}. If, at the time the pass runs,
118
- * the stream's local tips match the event's `fromTips` (it is caught up to where the event began), the event's
119
- * hydrated blocks are served back through a hot-block cache and the event's `toTips` is reported as the source tips
120
- * — so the triggered sync re-reads neither block bodies nor tips from the archiver. Otherwise the pass delegates
121
- * entirely to the source, and the periodic poll guarantees eventual catch-up.
20
+ * event (when the source exposes one) and uses it purely as a doorbell: each event triggers an immediate
21
+ * reconciliation pass instead of waiting for the next tick, while the periodic poll remains the correctness
22
+ * fallback. Every pass reads tips and blocks authoritatively from the source, so a missed, stale, or duplicated
23
+ * event only affects latency. Subsystems keep consuming the same {@link L2BlockStreamEvent}s; the archiver
24
+ * aggregate event is handled entirely here.
122
25
  */
123
26
  export class EventDrivenL2BlockStream {
124
- private readonly adapter: HotBlockSourceAdapter;
125
27
  private readonly blockStream: L2BlockStream;
126
- private readonly runningPromise: RunningPromise<L2BlockSourceUpdatedEvent>;
28
+ private readonly runningPromise: RunningPromise;
127
29
  private readonly emitter: ArchiverEmitter | undefined;
128
30
  private started = false;
129
31
 
130
- private readonly onSourceUpdated = (event: L2BlockSourceUpdatedEvent) => {
32
+ private readonly onSourceUpdated = () => {
131
33
  // Fire-and-forget: trigger coalesces with any in-flight or periodic pass (see RunningPromise.trigger), so a
132
34
  // burst of events does not run passes concurrently. Errors are swallowed by the inner stream's own handler.
133
- void this.runningPromise
134
- .trigger(event)
135
- .catch(err => this.log.error(`Error in event-triggered block stream sync`, err));
35
+ void this.runningPromise.trigger().catch(err => this.log.error(`Error in event-triggered block stream sync`, err));
136
36
  };
137
37
 
138
38
  constructor(
139
39
  source: L2BlockSource | L2BlockSourceEventEmitter,
140
- private readonly localData: L2BlockStreamLocalDataProvider,
40
+ localData: L2BlockStreamLocalDataProvider,
141
41
  handler: L2BlockStreamEventHandler,
142
42
  private readonly log = createLogger('types:event_driven_block_stream'),
143
43
  opts: L2BlockStreamOptions = {},
144
44
  ) {
145
- this.adapter = new HotBlockSourceAdapter(source, log);
146
45
  // The inner stream's own RunningPromise is never started; this wrapper owns the polling loop and drives the
147
46
  // stream through `sync()` (which runs `work()` directly when the inner loop is stopped).
148
- this.blockStream = new L2BlockStream(this.adapter, localData, handler, log, opts);
149
- this.runningPromise = new RunningPromise<L2BlockSourceUpdatedEvent>(
150
- this.runPass.bind(this),
151
- log,
152
- opts.pollIntervalMS ?? 1000,
153
- );
47
+ this.blockStream = new L2BlockStream(source, localData, handler, log, opts);
48
+ this.runningPromise = new RunningPromise(() => this.blockStream.sync(), log, opts.pollIntervalMS ?? 1000);
154
49
  this.emitter = getEmitter(source);
155
50
  }
156
51
 
@@ -176,32 +71,11 @@ export class EventDrivenL2BlockStream {
176
71
  }
177
72
 
178
73
  /**
179
- * Runs a synchronization pass now, bypassing the poll interval, and resolves once that pass completes. Concurrent
180
- * callers and periodic ticks coalesce onto a single pass; a caller that coalesces onto an already in-flight pass
181
- * can resolve against a pass that began just before it, so this guarantees freshness only up to that coalescing
182
- * window. The periodic poll and per-pass reorg handling make the gap a latency effect, never a correctness one.
74
+ * Runs a synchronization pass now, bypassing the poll interval. Resolves once a pass that started after this call
75
+ * completes, so the caller observes state at least as fresh as the moment it asked. Concurrent callers coalesce
76
+ * onto a single such pass. Rejects if the stream is stopped before that pass runs.
183
77
  */
184
78
  public sync(): Promise<void> {
185
79
  return this.runningPromise.trigger();
186
80
  }
187
-
188
- /**
189
- * Runs a single pass over the underlying block stream. When triggered by an aggregate event and the stream is
190
- * caught up to the event's pre-pass tips, the event's blocks and tips serve the pass directly; the fast path is
191
- * always disarmed afterwards so a subsequent poll-driven pass reads from the source.
192
- */
193
- private async runPass(event?: L2BlockSourceUpdatedEvent): Promise<void> {
194
- if (event) {
195
- const localTips = await this.localData.getL2Tips();
196
- if (localTipsMatch(localTips, event.fromTips)) {
197
- this.adapter.activate(event.blocksAdded, event.toTips);
198
- }
199
- }
200
-
201
- try {
202
- await this.blockStream.sync();
203
- } finally {
204
- this.adapter.deactivate();
205
- }
206
- }
207
81
  }
@@ -45,7 +45,7 @@ type CheckpointSimulationOverridesPlanInput = {
45
45
  /**
46
46
  * Builds the SimulationOverridesPlan describing the simulated L1 rollup state for a checkpoint's
47
47
  * enqueue-time simulations: `canProposeAt` (in Sequencer.doWork) and the propose-related sims
48
- * (validateBlockHeader, simulateProposeTx). The plan reflects "as if our pipelined parent
48
+ * (validateCheckpointHeader, simulateProposeTx). The plan reflects "as if our pipelined parent
49
49
  * checkpoint has landed and any required invalidation has executed" — the gap that needs to be
50
50
  * bridged at enqueue time.
51
51
  *
@@ -81,7 +81,7 @@ export async function buildCheckpointSimulationOverridesPlan(
81
81
  // `getEffectivePendingCheckpointNumber` silently collapses pending back to proven — producing
82
82
  // a spurious `Rollup__InvalidArchive` against the on-chain genesis archive. The other fields
83
83
  // (headerHash, outHash, payloadDigest) are not strictly load-bearing for `canProposeAt` /
84
- // `validateBlockHeader`, but mirroring the full cell keeps the simulation byte-faithful with
84
+ // `validateCheckpointHeader`, but mirroring the full cell keeps the simulation byte-faithful with
85
85
  // what the actual `propose()` send will observe, which is a defense against future reads
86
86
  // taking dependencies on them.
87
87
  builder.withPendingTempCheckpointLogFields({
@@ -38,9 +38,18 @@ export function validateCheckpoint(
38
38
  * accepted by L1.
39
39
  */
40
40
  maxBlocksPerCheckpoint?: number;
41
+ /**
42
+ * Whether to tolerate a zero-tx block after the first one. Defaults to false; the L1-sync ingest path
43
+ * passes true for the same reason it raises `maxBlocksPerCheckpoint` — such a checkpoint is already on
44
+ * L1, and refusing to ingest it would stall sync rather than undo it.
45
+ */
46
+ allowEmptyNonFirstBlocks?: boolean;
41
47
  },
42
48
  ): void {
43
- validateCheckpointStructure(checkpoint, { maxBlocksPerCheckpoint: opts.maxBlocksPerCheckpoint });
49
+ validateCheckpointStructure(checkpoint, {
50
+ maxBlocksPerCheckpoint: opts.maxBlocksPerCheckpoint,
51
+ allowEmptyNonFirstBlocks: opts.allowEmptyNonFirstBlocks,
52
+ });
44
53
  validateCheckpointLimits(checkpoint, opts);
45
54
  validateCheckpointBlocksLimits(checkpoint, opts);
46
55
  }
@@ -53,15 +62,16 @@ export function validateCheckpoint(
53
62
  * - Checkpoint lastArchiveRoot matches the first block's lastArchive root
54
63
  * - Sequential block numbers without gaps
55
64
  * - Sequential indexWithinCheckpoint starting at 0
65
+ * - Every block after the first carries at least one tx, unless `allowEmptyNonFirstBlocks` is set
56
66
  * - Archive root chaining between consecutive blocks
57
67
  * - Consistent slot number across all blocks
58
68
  * - Global variables (slot, timestamp, coinbase, feeRecipient, gasFees) match checkpoint header for each block
59
69
  */
60
70
  export function validateCheckpointStructure(
61
71
  checkpoint: Checkpoint,
62
- opts: { maxBlocksPerCheckpoint?: number } = {},
72
+ opts: { maxBlocksPerCheckpoint?: number; allowEmptyNonFirstBlocks?: boolean } = {},
63
73
  ): void {
64
- const { maxBlocksPerCheckpoint = MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT } = opts;
74
+ const { maxBlocksPerCheckpoint = MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT, allowEmptyNonFirstBlocks = false } = opts;
65
75
  const { blocks, number, slot } = checkpoint;
66
76
 
67
77
  if (blocks.length === 0) {
@@ -114,6 +124,18 @@ export function validateCheckpointStructure(
114
124
  }
115
125
 
116
126
  if (i > 0) {
127
+ // The only block-root circuit that proves a zero-tx block is `rollup_block_root_first_empty_tx`, which is
128
+ // pinned to index 0 of a checkpoint: it starts a fresh sponge blob and carries a non-zero in_hash, both of
129
+ // which the block-merge continuity checks forbid at any later index. An empty block past the first would
130
+ // therefore have no circuit able to prove it.
131
+ if (!allowEmptyNonFirstBlocks && block.body.txEffects.length === 0) {
132
+ throw new CheckpointValidationError(
133
+ `Block ${block.number} at index ${i} has no txs; only the first block of a checkpoint may be empty`,
134
+ number,
135
+ slot,
136
+ );
137
+ }
138
+
117
139
  const prev = blocks[i - 1];
118
140
  if (block.number !== prev.number + 1) {
119
141
  throw new CheckpointValidationError(
@@ -1,6 +1,7 @@
1
1
  import { type L1ContractsConfig, l1ContractsConfigMappings, validateSlotDurations } from '@aztec/ethereum/config';
2
2
  import { type EnvVar, pickConfigMappings } from '@aztec/foundation/config';
3
3
 
4
+ import { MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT } from '../deserialization/index.js';
4
5
  import type { SequencerConfig } from '../interfaces/configs.js';
5
6
  import {
6
7
  DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME,
@@ -168,6 +169,13 @@ export function validateNetworkConsensusConfig(config: NetworkConsensusConfig):
168
169
  if (config.maxBlocksPerCheckpoint < 1) {
169
170
  errors.push(`maxBlocksPerCheckpoint must be at least 1 (got ${config.maxBlocksPerCheckpoint})`);
170
171
  }
172
+ if (config.maxBlocksPerCheckpoint > MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT) {
173
+ errors.push(
174
+ `maxBlocksPerCheckpoint (${config.maxBlocksPerCheckpoint}) exceeds the ` +
175
+ `${MAX_ATTESTABLE_BLOCKS_PER_CHECKPOINT} blocks nodes will build or attest to, so the network would ` +
176
+ `reject block indices its own configuration admits`,
177
+ );
178
+ }
171
179
  if (config.checkpointProposalSyncGraceSeconds < 0) {
172
180
  errors.push(
173
181
  `checkpointProposalSyncGraceSeconds must be non-negative (got ${config.checkpointProposalSyncGraceSeconds})`,
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MAX_DEBUG_LOG_MEMORY_READS } from '@aztec/constants';
2
- import { type ConfigMappingsType, numberConfigHelper } from '@aztec/foundation/config';
2
+ import { type ConfigMappingsType, booleanConfigHelper, numberConfigHelper } from '@aztec/foundation/config';
3
3
 
4
4
  export const nodeRpcConfigMappings: ConfigMappingsType<NodeRPCConfig> = {
5
5
  rpcSimulatePublicMaxGasLimit: {
@@ -23,6 +23,21 @@ export const nodeRpcConfigMappings: ConfigMappingsType<NodeRPCConfig> = {
23
23
  description: 'Maximum allowed batch size for JSON RPC batch requests.',
24
24
  defaultValue: '1mb',
25
25
  },
26
+ rpcCorsAllowedOrigins: {
27
+ env: 'RPC_CORS_ALLOWED_ORIGINS',
28
+ description: 'Origins allowed to make credentialed cross-origin JSON RPC requests, separated by commas.',
29
+ parseEnv: (value: string) =>
30
+ value
31
+ .split(',')
32
+ .map(origin => origin.trim())
33
+ .filter(Boolean),
34
+ defaultValue: [],
35
+ },
36
+ rpcCorsAllowAnyOrigin: {
37
+ env: 'RPC_CORS_ALLOW_ANY_ORIGIN',
38
+ description: 'Allow credentialed cross-origin JSON RPC requests from any origin.',
39
+ ...booleanConfigHelper(false),
40
+ },
26
41
  };
27
42
 
28
43
  export type NodeRPCConfig = {
@@ -34,4 +49,15 @@ export type NodeRPCConfig = {
34
49
  rpcMaxBatchSize: number;
35
50
  /** The maximum body size the RPC server will accept */
36
51
  rpcMaxBodySize: string;
52
+ /** Origins allowed to make credentialed cross-origin requests to the RPC server. */
53
+ rpcCorsAllowedOrigins?: string[];
54
+ /** Whether to allow credentialed cross-origin requests from any origin. */
55
+ rpcCorsAllowAnyOrigin?: boolean;
37
56
  };
57
+
58
+ /** Resolves the CORS origin policy for an RPC server. */
59
+ export function getRpcCorsAllowedOrigins(
60
+ config: Pick<NodeRPCConfig, 'rpcCorsAllowedOrigins' | 'rpcCorsAllowAnyOrigin'>,
61
+ ): string[] {
62
+ return config.rpcCorsAllowAnyOrigin ? ['*'] : (config.rpcCorsAllowedOrigins ?? []);
63
+ }
@@ -15,7 +15,12 @@ import {
15
15
  } from '@aztec/foundation/branded-types';
16
16
  import type { Fr } from '@aztec/foundation/curves/bn254';
17
17
  import type { EthAddress } from '@aztec/foundation/eth-address';
18
- import { createSafeJsonRpcClient, makeFetch } from '@aztec/foundation/json-rpc/client';
18
+ import {
19
+ type JsonRpcFetch,
20
+ type JsonRpcFetchConfig,
21
+ createSafeJsonRpcClient,
22
+ makeFetch,
23
+ } from '@aztec/foundation/json-rpc/client';
19
24
  import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees';
20
25
 
21
26
  import { z } from 'zod';
@@ -793,13 +798,18 @@ export const AztecNodeApiSchema: ApiSchemaFor<AztecNode> = {
793
798
  export function createAztecNodeClient(
794
799
  url: string,
795
800
  versions: Partial<ComponentsVersions> = {},
796
- fetch = makeFetch([1, 2, 3], false),
801
+ fetch?: JsonRpcFetch,
797
802
  batchWindowMS = 0,
803
+ maxBatchSize?: number,
804
+ fetchOptions: JsonRpcFetchConfig = {},
798
805
  ): AztecNode {
806
+ const rpcFetch = fetch ?? makeFetch([1, 2, 3], false, undefined, fetchOptions);
807
+
799
808
  return createSafeJsonRpcClient<AztecNode>(url, AztecNodeApiSchema, {
800
809
  namespaceMethods: 'aztec',
801
- fetch,
810
+ fetch: rpcFetch,
802
811
  batchWindowMS,
812
+ maxBatchSize,
803
813
  onResponse: getVersioningResponseHandler(versions),
804
814
  });
805
815
  }
@@ -133,6 +133,11 @@ export interface SequencerConfig {
133
133
  skipBroadcastCheckpointProposal?: boolean;
134
134
  /** List of slots for which the sequencer will not produce a proposal (for testing only). Attestation paths are unaffected. */
135
135
  pauseProposingForSlots?: SlotNumber[];
136
+ /**
137
+ * Minimum number of connected p2p peers required to build and propose a checkpoint. Zero disables the
138
+ * check. Ignored when p2p is disabled by config.
139
+ */
140
+ minPeersToPropose?: number;
136
141
  }
137
142
 
138
143
  export const SequencerConfigSchema = zodFor<SequencerConfig>()(
@@ -186,6 +191,7 @@ export const SequencerConfigSchema = zodFor<SequencerConfig>()(
186
191
  skipBroadcastProposals: z.boolean().optional(),
187
192
  skipBroadcastCheckpointProposal: z.boolean().optional(),
188
193
  pauseProposingForSlots: z.array(SlotNumberSchema).optional(),
194
+ minPeersToPropose: z.number().nonnegative().optional(),
189
195
  }),
190
196
  );
191
197
 
@@ -29,6 +29,19 @@ export const PeerInfoSchema = z.discriminatedUnion('status', [
29
29
  }),
30
30
  ]);
31
31
 
32
+ /** Connectivity of the p2p stack: whether it is enabled at all, and how many peers are currently connected. */
33
+ export type P2PConnectivity = {
34
+ /** False when the node runs without a p2p stack (eg sandbox or single-node setups). */
35
+ enabled: boolean;
36
+ /** Number of peers currently connected. Always zero when p2p is disabled. */
37
+ connectedPeers: number;
38
+ };
39
+
40
+ export const P2PConnectivitySchema = z.object({
41
+ enabled: z.boolean(),
42
+ connectedPeers: z.number(),
43
+ });
44
+
32
45
  /** Exposed API to the P2P module. */
33
46
  export interface P2PApi {
34
47
  /**
@@ -54,6 +67,13 @@ export interface P2PApi {
54
67
  */
55
68
  getPeers(includePending?: boolean): Promise<PeerInfo[]>;
56
69
 
70
+ /**
71
+ * Returns whether the p2p stack is enabled on this node, and the number of peers it is connected to.
72
+ * Nodes running without p2p report `enabled: false`, so consumers can tell a disabled stack apart from a
73
+ * stack that is enabled but has no peers.
74
+ */
75
+ getP2PConnectivity(): Promise<P2PConnectivity>;
76
+
57
77
  /**
58
78
  * Queries the Attestation pool for checkpoint attestations for the given slot.
59
79
  *
@@ -116,4 +136,5 @@ export const P2PApiSchema: ApiSchemaFor<P2PApi> = {
116
136
  getPendingTxCount: z.function({ input: z.tuple([]), output: schemas.Integer }),
117
137
  getEncodedEnr: z.function({ input: z.tuple([]), output: z.string().optional() }),
118
138
  getPeers: z.function({ input: z.tuple([optional(z.boolean())]), output: z.array(PeerInfoSchema) }),
139
+ getP2PConnectivity: z.function({ input: z.tuple([]), output: P2PConnectivitySchema }),
119
140
  };
@@ -11,6 +11,8 @@ import type {
11
11
  CheckpointAttestation,
12
12
  CheckpointProposal,
13
13
  CheckpointProposalOptions,
14
+ ValidatedBlockProposal,
15
+ ValidatedCheckpointProposalCore,
14
16
  } from '@aztec/stdlib/p2p';
15
17
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
16
18
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
@@ -172,18 +174,18 @@ export interface Validator {
172
174
  ): Promise<CheckpointProposal>;
173
175
 
174
176
  /**
175
- * Validate a block proposal from a peer.
177
+ * Validate a block proposal from a peer that has already passed p2p ingress validation.
176
178
  * Note: Validators do NOT attest to individual blocks - attestations are only for checkpoint proposals.
177
179
  * @returns true if the proposal is valid, false otherwise
178
180
  */
179
- validateBlockProposal(proposal: BlockProposal, sender: PeerId): Promise<boolean>;
181
+ validateBlockProposal(proposal: ValidatedBlockProposal, sender: PeerId): Promise<boolean>;
180
182
 
181
183
  /**
182
- * Validate and attest to a checkpoint proposal from a peer.
184
+ * Validate and attest to a checkpoint proposal from a peer that has already passed p2p ingress validation.
183
185
  * @returns Checkpoint attestations if valid, undefined otherwise
184
186
  */
185
187
  attestToCheckpointProposal(
186
- proposal: CheckpointProposal,
188
+ proposal: ValidatedCheckpointProposalCore,
187
189
  sender: PeerId,
188
190
  ): Promise<CheckpointAttestation[] | undefined>;
189
191
 
package/src/p2p/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from './interface.js';
8
8
  export * from './signature_utils.js';
9
9
  export * from './signed_txs.js';
10
10
  export * from './topic_type.js';
11
+ export * from './validated_proposal.js';
11
12
  export * from './message_validator.js';
12
13
  export * from './peer_error.js';
13
14
  export * from './constants.js';
@@ -0,0 +1,43 @@
1
+ import type { Branded } from '@aztec/foundation/branded-types';
2
+
3
+ import type { BlockProposal } from './block_proposal.js';
4
+ import type { CheckpointProposalCore } from './checkpoint_proposal.js';
5
+
6
+ /**
7
+ * A block proposal that has passed p2p ingress validation.
8
+ *
9
+ * Downstream consumers (the validator client's proposal handlers) rely on that validation having already
10
+ * happened and do not repeat it, so this brand marks the proposals they are allowed to receive. It is a
11
+ * compile-time marker only: at runtime a `ValidatedBlockProposal` is just a `BlockProposal`.
12
+ */
13
+ export type ValidatedBlockProposal = Branded<BlockProposal, 'ValidatedBlockProposal'>;
14
+
15
+ /**
16
+ * Marks a block proposal as having passed p2p ingress validation.
17
+ *
18
+ * May only be called at a point where the gossipsub topic validator has accepted the proposal, which covers
19
+ * the signature context, the signature itself, the expected proposer for the slot, the index within the
20
+ * checkpoint, the tx field checks, and the receive-window timeliness check.
21
+ */
22
+ export function ValidatedBlockProposal(proposal: BlockProposal): ValidatedBlockProposal {
23
+ return proposal as ValidatedBlockProposal;
24
+ }
25
+
26
+ /**
27
+ * A checkpoint proposal (without its last block) that has passed p2p ingress validation.
28
+ *
29
+ * Downstream consumers (the validator client's proposal handlers) rely on that validation having already
30
+ * happened and do not repeat it, so this brand marks the proposals they are allowed to receive. It is a
31
+ * compile-time marker only: at runtime a `ValidatedCheckpointProposalCore` is just a `CheckpointProposalCore`.
32
+ */
33
+ export type ValidatedCheckpointProposalCore = Branded<CheckpointProposalCore, 'ValidatedCheckpointProposalCore'>;
34
+
35
+ /**
36
+ * Marks a checkpoint proposal as having passed p2p ingress validation.
37
+ *
38
+ * May only be called at a point where the gossipsub topic validator has accepted the proposal, which covers
39
+ * the expected proposer for the slot and the receive-window timeliness check.
40
+ */
41
+ export function ValidatedCheckpointProposalCore(proposal: CheckpointProposalCore): ValidatedCheckpointProposalCore {
42
+ return proposal as ValidatedCheckpointProposalCore;
43
+ }