@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.
- package/dest/block/l2_block_source.d.ts +3 -6
- package/dest/block/l2_block_source.d.ts.map +1 -1
- package/dest/block/l2_block_stream/event_driven_l2_block_stream.d.ts +11 -20
- package/dest/block/l2_block_stream/event_driven_l2_block_stream.d.ts.map +1 -1
- package/dest/block/l2_block_stream/event_driven_l2_block_stream.js +12 -116
- package/dest/checkpoint/simulation_overrides.d.ts +1 -1
- package/dest/checkpoint/simulation_overrides.js +2 -2
- package/dest/checkpoint/validate.d.ts +9 -1
- package/dest/checkpoint/validate.d.ts.map +1 -1
- package/dest/checkpoint/validate.js +11 -2
- package/dest/config/network-consensus-config.d.ts +1 -1
- package/dest/config/network-consensus-config.d.ts.map +1 -1
- package/dest/config/network-consensus-config.js +4 -0
- package/dest/config/node-rpc-config.d.ts +7 -1
- package/dest/config/node-rpc-config.d.ts.map +1 -1
- package/dest/config/node-rpc-config.js +17 -1
- package/dest/interfaces/aztec-node-admin.d.ts +2 -1
- package/dest/interfaces/aztec-node-admin.d.ts.map +1 -1
- package/dest/interfaces/aztec-node.d.ts +3 -2
- package/dest/interfaces/aztec-node.d.ts.map +1 -1
- package/dest/interfaces/aztec-node.js +8 -6
- package/dest/interfaces/configs.d.ts +7 -1
- package/dest/interfaces/configs.d.ts.map +1 -1
- package/dest/interfaces/configs.js +2 -1
- package/dest/interfaces/p2p.d.ts +18 -1
- package/dest/interfaces/p2p.d.ts.map +1 -1
- package/dest/interfaces/p2p.js +8 -0
- package/dest/interfaces/validator.d.ts +6 -6
- package/dest/interfaces/validator.d.ts.map +1 -1
- package/dest/p2p/index.d.ts +2 -1
- package/dest/p2p/index.d.ts.map +1 -1
- package/dest/p2p/index.js +1 -0
- package/dest/p2p/validated_proposal.d.ts +35 -0
- package/dest/p2p/validated_proposal.d.ts.map +1 -0
- package/dest/p2p/validated_proposal.js +17 -0
- package/package.json +8 -8
- package/src/block/l2_block_source.ts +2 -5
- package/src/block/l2_block_stream/event_driven_l2_block_stream.ts +17 -143
- package/src/checkpoint/simulation_overrides.ts +2 -2
- package/src/checkpoint/validate.ts +25 -3
- package/src/config/network-consensus-config.ts +8 -0
- package/src/config/node-rpc-config.ts +27 -1
- package/src/interfaces/aztec-node.ts +13 -3
- package/src/interfaces/configs.ts +6 -0
- package/src/interfaces/p2p.ts +21 -0
- package/src/interfaces/validator.ts +6 -4
- package/src/p2p/index.ts +1 -0
- package/src/p2p/validated_proposal.ts +43 -0
|
@@ -1,156 +1,51 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
17
|
-
import { L2BlockStream, type L2BlockStreamOptions
|
|
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)
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
|
28
|
+
private readonly runningPromise: RunningPromise;
|
|
127
29
|
private readonly emitter: ArchiverEmitter | undefined;
|
|
128
30
|
private started = false;
|
|
129
31
|
|
|
130
|
-
private readonly onSourceUpdated = (
|
|
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
|
-
|
|
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(
|
|
149
|
-
this.runningPromise = new RunningPromise
|
|
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
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
-
* (
|
|
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
|
-
// `
|
|
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, {
|
|
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 {
|
|
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
|
|
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
|
|
package/src/interfaces/p2p.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
+
}
|