@aztec/aztec-node 5.3.0-nightly.20260909 → 5.3.0-nightly.20260910
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/aztec-node/next_block/index.d.ts +4 -0
- package/dest/aztec-node/next_block/index.d.ts.map +1 -0
- package/dest/aztec-node/next_block/index.js +3 -0
- package/dest/aztec-node/next_block/next_block_fee_cache.d.ts +110 -0
- package/dest/aztec-node/next_block/next_block_fee_cache.d.ts.map +1 -0
- package/dest/aztec-node/next_block/next_block_fee_cache.js +201 -0
- package/dest/aztec-node/next_block/next_block_planner.d.ts +78 -0
- package/dest/aztec-node/next_block/next_block_planner.d.ts.map +1 -0
- package/dest/aztec-node/next_block/next_block_planner.js +109 -0
- package/dest/aztec-node/next_block/next_block_predictor.d.ts +91 -0
- package/dest/aztec-node/next_block/next_block_predictor.d.ts.map +1 -0
- package/dest/aztec-node/next_block/next_block_predictor.js +146 -0
- package/dest/aztec-node/next_block/test_helpers.d.ts +40 -0
- package/dest/aztec-node/next_block/test_helpers.d.ts.map +1 -0
- package/dest/aztec-node/next_block/test_helpers.js +100 -0
- package/dest/aztec-node/node_public_calls_simulator.d.ts +10 -48
- package/dest/aztec-node/node_public_calls_simulator.d.ts.map +1 -1
- package/dest/aztec-node/node_public_calls_simulator.js +56 -164
- package/dest/aztec-node/server.d.ts +18 -2
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +34 -12
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +17 -1
- package/dest/index.d.ts +2 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -0
- package/package.json +28 -28
- package/src/aztec-node/next_block/index.ts +3 -0
- package/src/aztec-node/next_block/next_block_fee_cache.ts +279 -0
- package/src/aztec-node/next_block/next_block_planner.ts +198 -0
- package/src/aztec-node/next_block/next_block_predictor.ts +158 -0
- package/src/aztec-node/next_block/test_helpers.ts +115 -0
- package/src/aztec-node/node_public_calls_simulator.ts +78 -218
- package/src/aztec-node/server.ts +32 -9
- package/src/factory.ts +15 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { EpochCacheInterface } from '@aztec/epoch-cache';
|
|
2
|
+
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
|
+
import type { L1SyncPoint, L2BlockSource, L2Frontier } from '@aztec/stdlib/block';
|
|
5
|
+
import type { GasFees } from '@aztec/stdlib/gas';
|
|
6
|
+
import { GlobalVariables } from '@aztec/stdlib/tx';
|
|
7
|
+
|
|
8
|
+
import { NextBlockFeeCache, type NextBlockFeeCacheDeps } from './next_block_fee_cache.js';
|
|
9
|
+
import { type NextBlockPlan, computeBoundaryFeeKey, getClockSlot, planNextBlock } from './next_block_planner.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How long a fee quote waits for a boundary refresh before answering with what it already has. Long enough for
|
|
13
|
+
* a slow but alive L1 RPC; short enough that an L1 outage does not turn every quote into a multi-second RPC.
|
|
14
|
+
*/
|
|
15
|
+
export const QUOTE_MAX_WAIT_MS = 5000;
|
|
16
|
+
|
|
17
|
+
/** The next block as this node predicts it: how it sits on the chain, plus the globals it would carry. */
|
|
18
|
+
export type NextBlockPrediction = {
|
|
19
|
+
plan: NextBlockPlan;
|
|
20
|
+
/** Snapshot the plan was derived from, so callers can pin their own reads to the same instant. */
|
|
21
|
+
frontier: L2Frontier;
|
|
22
|
+
globals: GlobalVariables;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Dependencies required to build a {@link NextBlockPredictor}. */
|
|
26
|
+
export interface NextBlockPredictorDeps {
|
|
27
|
+
blockSource: L2BlockSource;
|
|
28
|
+
feeCache: NextBlockFeeCache;
|
|
29
|
+
epochCache: EpochCacheInterface;
|
|
30
|
+
log?: Logger;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Answers "what would the next block look like" for everything on the RPC side: the public simulation forks and
|
|
35
|
+
* executes against this prediction, and the fee quote reports the fee it carries.
|
|
36
|
+
*
|
|
37
|
+
* The plan is re-derived from a fresh archiver snapshot on every call — it is a handful of in-memory reads, and
|
|
38
|
+
* a stale plan would fork the wrong block, target the wrong checkpoint's L1-to-L2 messages, or miss that an
|
|
39
|
+
* in-progress checkpoint froze a different fee. Only the L1-derived part, the checkpoint globals of a block that
|
|
40
|
+
* opens a fresh checkpoint, is cached (see {@link NextBlockFeeCache}).
|
|
41
|
+
*
|
|
42
|
+
* Deliberately not used by the sequencer: its slot policy is stricter (it declines to build rather than
|
|
43
|
+
* predicting inclusion) and a stale fee would make L1 reject its checkpoint.
|
|
44
|
+
*/
|
|
45
|
+
export class NextBlockPredictor {
|
|
46
|
+
private readonly blockSource: L2BlockSource;
|
|
47
|
+
private readonly feeCache: NextBlockFeeCache;
|
|
48
|
+
private readonly epochCache: EpochCacheInterface;
|
|
49
|
+
private readonly log: Logger;
|
|
50
|
+
|
|
51
|
+
constructor(deps: NextBlockPredictorDeps) {
|
|
52
|
+
this.blockSource = deps.blockSource;
|
|
53
|
+
this.feeCache = deps.feeCache;
|
|
54
|
+
this.epochCache = deps.epochCache;
|
|
55
|
+
this.log = deps.log ?? createLogger('node:next-block-predictor');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds a predictor together with the fee cache it reads from. It takes the cache's dependencies rather than
|
|
60
|
+
* {@link NextBlockPredictorDeps} because the cache is constructed here; everything the predictor itself needs is
|
|
61
|
+
* a subset of them.
|
|
62
|
+
*/
|
|
63
|
+
public static create(deps: NextBlockFeeCacheDeps): NextBlockPredictor {
|
|
64
|
+
const feeCache = new NextBlockFeeCache({ ...deps, log: deps.log?.createChild('fee-cache') });
|
|
65
|
+
return new NextBlockPredictor({
|
|
66
|
+
blockSource: deps.blockSource,
|
|
67
|
+
feeCache,
|
|
68
|
+
epochCache: deps.epochCache,
|
|
69
|
+
log: deps.log,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public start(pollingIntervalMs?: number): void {
|
|
74
|
+
this.feeCache.start(pollingIntervalMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public stop(): Promise<void> {
|
|
78
|
+
return this.feeCache.stop();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Plans the next block and builds the globals it would carry, in one of two ways, mirroring how the sequencer
|
|
83
|
+
* builds the next block:
|
|
84
|
+
*
|
|
85
|
+
* - **Continuing an in-progress checkpoint**: every block in a checkpoint shares the same
|
|
86
|
+
* `CheckpointGlobalVariables`, so the latest proposed block's globals are copied verbatim — including the
|
|
87
|
+
* proposer's real coinbase and fee recipient — with only the block number bumped. No L1 involved.
|
|
88
|
+
* - **Opening a new checkpoint**: the globals are priced for the slot the next block will land in, under the
|
|
89
|
+
* same overrides plan the sequencer applies, so the simulated mana min fee matches what the sequencer will
|
|
90
|
+
* write into the block header. Coinbase and fee recipient stay zero: the future proposer's payout addresses
|
|
91
|
+
* are unknowable.
|
|
92
|
+
*
|
|
93
|
+
* Waits without a bound for a boundary fee it does not have cached, and surfaces the L1 failure if that read
|
|
94
|
+
* fails, as a simulation did before this cache existed.
|
|
95
|
+
*/
|
|
96
|
+
public async predict(): Promise<NextBlockPrediction> {
|
|
97
|
+
const { frontier, plan, key } = await this.planFromFrontier();
|
|
98
|
+
const blockNumber = BlockNumber.add(plan.latestBlockNumber, 1);
|
|
99
|
+
if (!key) {
|
|
100
|
+
return { plan, frontier, globals: this.copyGlobalsFromLatestBlock(frontier, blockNumber) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const checkpointGlobals = await this.feeCache.getBoundaryGlobals(key, frontier);
|
|
104
|
+
if (!checkpointGlobals) {
|
|
105
|
+
throw new Error(`Could not price the next block at slot ${key.targetSlot}: no boundary fee available`);
|
|
106
|
+
}
|
|
107
|
+
return { plan, frontier, globals: GlobalVariables.from({ blockNumber, ...checkpointGlobals }) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The mana min fee the next block will charge, for wallets to pad and pay. Mid-checkpoint this is the fee the
|
|
112
|
+
* in-progress checkpoint froze into its first block, which no forward-looking L1 projection can see. At a
|
|
113
|
+
* boundary it is the cached L1 price, waited on for at most {@link QUOTE_MAX_WAIT_MS}; undefined when the node
|
|
114
|
+
* has nothing usable, in which case the caller falls back to the fee provider's projections alone.
|
|
115
|
+
*
|
|
116
|
+
* Returns the L1 block the answer describes so the caller can tag its own fee reads with the same anchor.
|
|
117
|
+
*/
|
|
118
|
+
public async quoteMinFees(): Promise<{ fees: GasFees; l1SyncPoint: L1SyncPoint | undefined } | undefined> {
|
|
119
|
+
const { frontier, plan, key } = await this.planFromFrontier();
|
|
120
|
+
if (!key) {
|
|
121
|
+
const fees = frontier.latestBlockHeader?.globalVariables.gasFees;
|
|
122
|
+
if (!fees) {
|
|
123
|
+
this.log.warn(`Cannot quote the next block fee: frontier reports no header for its proposed tip`, {
|
|
124
|
+
blockNumber: plan.latestBlockNumber,
|
|
125
|
+
});
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return { fees, l1SyncPoint: frontier.l1SyncPoint };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const checkpointGlobals = await this.feeCache.getBoundaryGlobals(key, frontier, { maxWaitMs: QUOTE_MAX_WAIT_MS });
|
|
132
|
+
return checkpointGlobals ? { fees: checkpointGlobals.gasFees, l1SyncPoint: frontier.l1SyncPoint } : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Plans the next block from a fresh archiver snapshot; `key` is set only when that block opens a checkpoint. */
|
|
136
|
+
private async planFromFrontier() {
|
|
137
|
+
const frontier = await this.blockSource.getL2Frontier();
|
|
138
|
+
const plan = planNextBlock(frontier, getClockSlot(this.epochCache));
|
|
139
|
+
const key = computeBoundaryFeeKey(plan, frontier.pendingChainValidationStatus);
|
|
140
|
+
return { frontier, plan, key };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The header comes from the same snapshot as the tips, so it cannot describe a different block than the
|
|
145
|
+
* proposed tip. A missing header at a non-genesis proposed tip is an invariant violation and throws rather
|
|
146
|
+
* than falling through to the new-checkpoint path: a fork at the proposed tip already contains the ongoing
|
|
147
|
+
* checkpoint's L1-to-L2 messages, so a caller inserting the next checkpoint's messages would append them a
|
|
148
|
+
* second time.
|
|
149
|
+
*/
|
|
150
|
+
private copyGlobalsFromLatestBlock(frontier: L2Frontier, blockNumber: BlockNumber): GlobalVariables {
|
|
151
|
+
if (!frontier.latestBlockHeader) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Cannot predict the next block: frontier reports proposed tip ${frontier.tips.proposed.number} but carries no header`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return GlobalVariables.from({ ...frontier.latestBlockHeader.globalVariables, blockNumber });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { FeeHeader } from '@aztec/ethereum/contracts';
|
|
2
|
+
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
|
+
import {
|
|
5
|
+
BlockHash,
|
|
6
|
+
type L1SyncPoint,
|
|
7
|
+
type L2Frontier,
|
|
8
|
+
type L2Tips,
|
|
9
|
+
type ValidateCheckpointResult,
|
|
10
|
+
} from '@aztec/stdlib/block';
|
|
11
|
+
import { L1PublishedData, type ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
|
|
12
|
+
import { GasFees } from '@aztec/stdlib/gas';
|
|
13
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
14
|
+
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
15
|
+
import { BlockHeader, GlobalVariables } from '@aztec/stdlib/tx';
|
|
16
|
+
|
|
17
|
+
/** Shape of the frontier snapshot a next-block test wants; every field has a sensible default. */
|
|
18
|
+
export type L2FrontierArgs = {
|
|
19
|
+
proposed: BlockNumber;
|
|
20
|
+
checkpointedBlock: BlockNumber;
|
|
21
|
+
checkpointed: CheckpointNumber;
|
|
22
|
+
/** Slot of the checkpointed checkpoint, the floor for the next block's slot. Omit for the genesis shape. */
|
|
23
|
+
checkpointedTipSlot?: SlotNumber;
|
|
24
|
+
proposedCheckpoint?: ProposedCheckpointData;
|
|
25
|
+
/** Globals of the proposed tip's header, copied verbatim when the next block continues a checkpoint. */
|
|
26
|
+
latestBlockGlobals?: { slotNumber: SlotNumber; gasFees?: GasFees };
|
|
27
|
+
/** Omit the proposed tip's header from the snapshot, an invariant violation the predictor must reject. */
|
|
28
|
+
omitLatestBlockHeader?: boolean;
|
|
29
|
+
pendingChainValidationStatus?: ValidateCheckpointResult;
|
|
30
|
+
/** L1 block the archiver's snapshot reflects; fee reads must be pinned to it. */
|
|
31
|
+
l1SyncPoint?: L1SyncPoint;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Stable per-block hash, so tips and the frontier agree on block identity. */
|
|
35
|
+
export const blockHashOf = (blockNumber: BlockNumber): BlockHash => new BlockHash(new Fr(1000 + blockNumber));
|
|
36
|
+
|
|
37
|
+
const makeTips = (args: L2FrontierArgs): L2Tips => {
|
|
38
|
+
const blockId = (number: BlockNumber) => ({ number, hash: blockHashOf(number).toString() });
|
|
39
|
+
const checkpointId = (number: CheckpointNumber) => ({ number, hash: `0xc${number}` });
|
|
40
|
+
return {
|
|
41
|
+
proposed: blockId(args.proposed),
|
|
42
|
+
checkpointed: { block: blockId(args.checkpointedBlock), checkpoint: checkpointId(args.checkpointed) },
|
|
43
|
+
proven: { block: blockId(BlockNumber.ZERO), checkpoint: checkpointId(args.checkpointed) },
|
|
44
|
+
finalized: { block: blockId(BlockNumber.ZERO), checkpoint: checkpointId(args.checkpointed) },
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const makeFrontier = (args: L2FrontierArgs): L2Frontier => ({
|
|
49
|
+
tips: makeTips(args),
|
|
50
|
+
proposedCheckpoint: args.proposedCheckpoint,
|
|
51
|
+
l1SyncPoint: args.l1SyncPoint,
|
|
52
|
+
latestBlockHeader:
|
|
53
|
+
args.omitLatestBlockHeader || !args.latestBlockGlobals
|
|
54
|
+
? undefined
|
|
55
|
+
: BlockHeader.empty({
|
|
56
|
+
globalVariables: GlobalVariables.empty({
|
|
57
|
+
blockNumber: args.proposed,
|
|
58
|
+
slotNumber: args.latestBlockGlobals.slotNumber,
|
|
59
|
+
gasFees: args.latestBlockGlobals.gasFees ?? GasFees.empty(),
|
|
60
|
+
}),
|
|
61
|
+
}),
|
|
62
|
+
checkpointedCheckpoint:
|
|
63
|
+
args.checkpointedTipSlot === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: {
|
|
66
|
+
header: CheckpointHeader.empty({ slotNumber: args.checkpointedTipSlot }),
|
|
67
|
+
l1: new L1PublishedData(1n, 0n, `0x`),
|
|
68
|
+
},
|
|
69
|
+
pendingChainValidationStatus: args.pendingChainValidationStatus ?? { valid: true },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export function makeFeeHeader(): FeeHeader {
|
|
73
|
+
return { excessMana: 0n, manaUsed: 0n, ethPerFeeAsset: 0n, congestionCost: 0n, proverCost: 0n };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function makeProposedCheckpointData(args: {
|
|
77
|
+
checkpointNumber: CheckpointNumber;
|
|
78
|
+
lastBlock: BlockNumber;
|
|
79
|
+
slotNumber?: SlotNumber;
|
|
80
|
+
archiveRoot?: Fr;
|
|
81
|
+
totalManaUsed?: bigint;
|
|
82
|
+
feeAssetPriceModifier?: bigint;
|
|
83
|
+
checkpointOutHash?: Fr;
|
|
84
|
+
}): ProposedCheckpointData {
|
|
85
|
+
return {
|
|
86
|
+
checkpointNumber: args.checkpointNumber,
|
|
87
|
+
header: CheckpointHeader.empty({ slotNumber: args.slotNumber ?? SlotNumber(0) }),
|
|
88
|
+
startBlock: args.lastBlock,
|
|
89
|
+
blockCount: 1,
|
|
90
|
+
totalManaUsed: args.totalManaUsed ?? 555n,
|
|
91
|
+
feeAssetPriceModifier: args.feeAssetPriceModifier ?? 7n,
|
|
92
|
+
archive: new AppendOnlyTreeSnapshot(args.archiveRoot ?? Fr.ZERO, 0),
|
|
93
|
+
checkpointOutHash: args.checkpointOutHash ?? Fr.fromString('0xfeed'),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function makeInvalidStatus(firstInvalid: CheckpointNumber): ValidateCheckpointResult {
|
|
98
|
+
return {
|
|
99
|
+
valid: false,
|
|
100
|
+
checkpoint: {
|
|
101
|
+
archive: Fr.random(),
|
|
102
|
+
lastArchive: Fr.random(),
|
|
103
|
+
slotNumber: SlotNumber(10),
|
|
104
|
+
checkpointNumber: firstInvalid,
|
|
105
|
+
timestamp: 0n,
|
|
106
|
+
},
|
|
107
|
+
committee: [],
|
|
108
|
+
epoch: EpochNumber.ZERO,
|
|
109
|
+
seed: 0n,
|
|
110
|
+
attestors: [],
|
|
111
|
+
attestations: [],
|
|
112
|
+
verbatimAttestations: { signatureIndices: '0x', signaturesOrAddresses: '0x' },
|
|
113
|
+
reason: 'insufficient-attestations',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
import { L1ToL2MessagesNotReadyError } from '@aztec/archiver';
|
|
2
|
-
import {
|
|
3
|
-
import type { EpochCacheInterface } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
5
|
-
type RollupContract,
|
|
6
|
-
SimulationOverridesBuilder,
|
|
7
|
-
type SimulationOverridesPlan,
|
|
8
|
-
} from '@aztec/ethereum/contracts';
|
|
9
|
-
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
10
|
-
import { compactArray } from '@aztec/foundation/collection';
|
|
2
|
+
import type { CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
11
3
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
12
|
-
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
13
4
|
import { BadRequestError } from '@aztec/foundation/json-rpc';
|
|
14
5
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
15
6
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
16
7
|
import { isErrorClass } from '@aztec/foundation/types';
|
|
17
8
|
import { PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server';
|
|
18
9
|
import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm';
|
|
19
|
-
import {
|
|
20
|
-
import type { L2BlockSource, L2Tips } from '@aztec/stdlib/block';
|
|
21
|
-
import { type ProposedCheckpointData, buildCheckpointSimulationOverridesPlan } from '@aztec/stdlib/checkpoint';
|
|
10
|
+
import { BlockHash } from '@aztec/stdlib/block';
|
|
22
11
|
import type { ContractDataSource } from '@aztec/stdlib/contract';
|
|
23
12
|
import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
24
13
|
import { type L1ToL2MessageSource, appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging';
|
|
25
|
-
import type
|
|
26
|
-
import {
|
|
27
|
-
type GlobalVariableBuilder,
|
|
28
|
-
GlobalVariables,
|
|
29
|
-
PublicSimulationOutput,
|
|
30
|
-
type SimulationOverrides,
|
|
31
|
-
type Tx,
|
|
32
|
-
} from '@aztec/stdlib/tx';
|
|
14
|
+
import { type GlobalVariables, PublicSimulationOutput, type SimulationOverrides, type Tx } from '@aztec/stdlib/tx';
|
|
33
15
|
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
16
|
+
import { WorldStateSynchronizerError } from '@aztec/world-state';
|
|
34
17
|
|
|
18
|
+
import type { NextBlockPlan, NextBlockPredictor } from './next_block/index.js';
|
|
35
19
|
import { applyPublicDataOverrides } from './public_data_overrides.js';
|
|
36
20
|
|
|
21
|
+
/** Attempts at planning the next block on a chain the world state agrees with, before giving up. */
|
|
22
|
+
const MAX_PREDICTION_ATTEMPTS = 2;
|
|
23
|
+
|
|
37
24
|
/** Config fields the simulator needs — a narrow subset of `AztecNodeConfig`. */
|
|
38
25
|
export interface NodePublicCallsSimulatorConfig {
|
|
39
26
|
/** Maximum total gas limit accepted for an incoming simulation. */
|
|
@@ -44,63 +31,45 @@ export interface NodePublicCallsSimulatorConfig {
|
|
|
44
31
|
|
|
45
32
|
/** Dependencies required to build a {@link NodePublicCallsSimulator}. */
|
|
46
33
|
export interface NodePublicCallsSimulatorDeps {
|
|
47
|
-
blockSource: L2BlockSource;
|
|
48
34
|
worldStateSynchronizer: WorldStateSynchronizer;
|
|
49
35
|
l1ToL2MessageSource: L1ToL2MessageSource;
|
|
50
36
|
contractDataSource: ContractDataSource;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Rollup contract used to build the fee-relevant L1 state overrides when opening a new checkpoint.
|
|
54
|
-
* Only needed when a proposed parent checkpoint exists (pipelining) or the pending chain is invalid;
|
|
55
|
-
* may be omitted in environments that never reach those states (e.g. TXE). When omitted, those paths
|
|
56
|
-
* degrade to a pinned-tips plan (non-pipelined fees) instead.
|
|
57
|
-
*/
|
|
58
|
-
rollupContract?: RollupContract;
|
|
59
|
-
epochCache: EpochCacheInterface;
|
|
60
|
-
signatureContext: CoordinationSignatureContext;
|
|
37
|
+
predictor: NextBlockPredictor;
|
|
61
38
|
config: NodePublicCallsSimulatorConfig;
|
|
62
39
|
telemetry?: TelemetryClient;
|
|
63
40
|
log?: Logger;
|
|
64
41
|
}
|
|
65
42
|
|
|
43
|
+
/** The next block, planned and priced, on a chain the world state has caught up with. */
|
|
44
|
+
type PreparedNextBlock = {
|
|
45
|
+
plan: NextBlockPlan;
|
|
46
|
+
globals: GlobalVariables;
|
|
47
|
+
/** L1-to-L2 messages of the checkpoint the next block opens; undefined when it continues one. */
|
|
48
|
+
messages: Fr[] | undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
66
51
|
/**
|
|
67
52
|
* Simulates the public part of a transaction against a fresh world-state fork.
|
|
68
53
|
*
|
|
69
|
-
* Extracted from `AztecNodeService` so
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* - **When the next block continues an in-progress checkpoint** (the latest proposed block is ahead of
|
|
74
|
-
* the proposed-checkpoint frontier): every block in a checkpoint shares the same
|
|
75
|
-
* `CheckpointGlobalVariables`, so we copy the latest proposed block's globals verbatim and only
|
|
76
|
-
* bump the block number. No L1 calls, no L1-to-L2 message insertion.
|
|
77
|
-
* - **When the next block opens a new checkpoint** (the latest proposed block coincides with the
|
|
78
|
-
* proposed-checkpoint frontier): we compute fresh globals for the slot the next block will land in,
|
|
79
|
-
* applying the same `SimulationOverridesPlan` the sequencer applies so the simulated mana min fee
|
|
80
|
-
* matches what the sequencer will write into the block header.
|
|
54
|
+
* Extracted from `AztecNodeService` so forking and execution can be unit-tested without standing up the whole
|
|
55
|
+
* node, and to keep `server.ts` smaller. Which block is simulated, and the globals it carries, are decided by
|
|
56
|
+
* the {@link NextBlockPredictor}: the simulator's job is to fork the chain that plan describes, insert the
|
|
57
|
+
* L1-to-L2 messages a checkpoint-opening block would see, and run the processor.
|
|
81
58
|
*/
|
|
82
59
|
export class NodePublicCallsSimulator {
|
|
83
|
-
private readonly blockSource: L2BlockSource;
|
|
84
60
|
private readonly worldStateSynchronizer: WorldStateSynchronizer;
|
|
85
61
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource;
|
|
86
62
|
private readonly contractDataSource: ContractDataSource;
|
|
87
|
-
private readonly
|
|
88
|
-
private readonly rollupContract: RollupContract | undefined;
|
|
89
|
-
private readonly epochCache: EpochCacheInterface;
|
|
90
|
-
private readonly signatureContext: CoordinationSignatureContext;
|
|
63
|
+
private readonly predictor: NextBlockPredictor;
|
|
91
64
|
private readonly config: NodePublicCallsSimulatorConfig;
|
|
92
65
|
private readonly telemetry: TelemetryClient;
|
|
93
66
|
private readonly log: Logger;
|
|
94
67
|
|
|
95
68
|
constructor(deps: NodePublicCallsSimulatorDeps) {
|
|
96
|
-
this.blockSource = deps.blockSource;
|
|
97
69
|
this.worldStateSynchronizer = deps.worldStateSynchronizer;
|
|
98
70
|
this.l1ToL2MessageSource = deps.l1ToL2MessageSource;
|
|
99
71
|
this.contractDataSource = deps.contractDataSource;
|
|
100
|
-
this.
|
|
101
|
-
this.rollupContract = deps.rollupContract;
|
|
102
|
-
this.epochCache = deps.epochCache;
|
|
103
|
-
this.signatureContext = deps.signatureContext;
|
|
72
|
+
this.predictor = deps.predictor;
|
|
104
73
|
this.config = deps.config;
|
|
105
74
|
this.telemetry = deps.telemetry ?? getTelemetryClient();
|
|
106
75
|
this.log = deps.log ?? createLogger('node:public-calls-simulator');
|
|
@@ -132,29 +101,7 @@ export class NodePublicCallsSimulator {
|
|
|
132
101
|
}
|
|
133
102
|
|
|
134
103
|
const txHash = tx.getTxHash();
|
|
135
|
-
const
|
|
136
|
-
this.blockSource.getL2Tips(),
|
|
137
|
-
this.blockSource.getProposedCheckpointData(),
|
|
138
|
-
]);
|
|
139
|
-
const latestBlockNumber = l2Tips.proposed.number;
|
|
140
|
-
const blockNumber = BlockNumber.add(latestBlockNumber, 1);
|
|
141
|
-
|
|
142
|
-
// Terminating block of the proposed-checkpoint frontier. `getProposedCheckpointData()` returns the
|
|
143
|
-
// leading proposed (not-yet-L1-confirmed) checkpoint, whose last block is `startBlock + blockCount
|
|
144
|
-
// - 1`; with no proposed checkpoint the frontier coincides with the checkpointed tip.
|
|
145
|
-
const proposedCheckpointLastBlock = proposedCheckpointData
|
|
146
|
-
? BlockNumber.add(proposedCheckpointData.startBlock, proposedCheckpointData.blockCount - 1)
|
|
147
|
-
: l2Tips.checkpointed.block.number;
|
|
148
|
-
|
|
149
|
-
// The next block continues the in-progress checkpoint when the latest proposed block is ahead of
|
|
150
|
-
// the proposed-checkpoint terminating block; it opens a new checkpoint when they coincide.
|
|
151
|
-
const atCheckpointBoundary = proposedCheckpointLastBlock === l2Tips.proposed.number;
|
|
152
|
-
|
|
153
|
-
// `targetCheckpoint` is the checkpoint whose L1-to-L2 messages must be inserted into the fork
|
|
154
|
-
// before simulation. Only set when opening a new checkpoint, where the next block is its first block.
|
|
155
|
-
const { globalVariables: newGlobalVariables, targetCheckpoint } = atCheckpointBoundary
|
|
156
|
-
? await this.buildGlobalVariablesForNewCheckpoint(l2Tips, proposedCheckpointData, blockNumber)
|
|
157
|
-
: { globalVariables: await this.copyGlobalVariablesFromLatestProposedBlock(latestBlockNumber, blockNumber) };
|
|
104
|
+
const { plan, globals, messages } = await this.prepareNextBlock();
|
|
158
105
|
|
|
159
106
|
const publicProcessorFactory = new PublicProcessorFactory(
|
|
160
107
|
this.contractDataSource,
|
|
@@ -164,26 +111,20 @@ export class NodePublicCallsSimulator {
|
|
|
164
111
|
);
|
|
165
112
|
|
|
166
113
|
this.log.verbose(`Simulating public calls for tx ${txHash}`, {
|
|
167
|
-
globalVariables:
|
|
114
|
+
globalVariables: globals.toInspect(),
|
|
168
115
|
txHash,
|
|
169
|
-
blockNumber,
|
|
170
|
-
atCheckpointBoundary,
|
|
116
|
+
blockNumber: globals.blockNumber,
|
|
117
|
+
atCheckpointBoundary: plan.newCheckpoint !== undefined,
|
|
171
118
|
});
|
|
172
119
|
|
|
173
|
-
// Ensure world-state has caught up with the latest block we loaded from the archiver
|
|
174
|
-
await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
|
|
175
|
-
|
|
176
|
-
const nextCheckpointMessages = await this.getNextCheckpointMessages(targetCheckpoint);
|
|
177
|
-
|
|
178
120
|
// Request a new fork of the world state at the latest block number, and apply any overrides and next checkpoint messages to it before simulation
|
|
179
|
-
await using merkleTreeFork = await this.worldStateSynchronizer.fork(latestBlockNumber);
|
|
121
|
+
await using merkleTreeFork = await this.worldStateSynchronizer.fork(plan.latestBlockNumber);
|
|
180
122
|
|
|
181
|
-
if (
|
|
182
|
-
this.log.debug(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
186
|
-
await appendL1ToL2MessagesToTree(merkleTreeFork, nextCheckpointMessages);
|
|
123
|
+
if (messages !== undefined) {
|
|
124
|
+
this.log.debug(`Appending ${messages.length} L1-to-L2 messages to the world state tree for the next checkpoint`, {
|
|
125
|
+
checkpointNumber: plan.newCheckpoint?.targetCheckpoint,
|
|
126
|
+
});
|
|
127
|
+
await appendL1ToL2MessagesToTree(merkleTreeFork, messages);
|
|
187
128
|
}
|
|
188
129
|
|
|
189
130
|
await applyPublicDataOverrides(merkleTreeFork, overrides?.publicStorage);
|
|
@@ -203,7 +144,7 @@ export class NodePublicCallsSimulator {
|
|
|
203
144
|
if (overrides?.contracts) {
|
|
204
145
|
contractsDB.addContracts(Object.values(overrides.contracts).map(({ instance }) => instance));
|
|
205
146
|
}
|
|
206
|
-
const processor = publicProcessorFactory.create(merkleTreeFork,
|
|
147
|
+
const processor = publicProcessorFactory.create(merkleTreeFork, globals, config, contractsDB);
|
|
207
148
|
|
|
208
149
|
// REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
|
|
209
150
|
const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]);
|
|
@@ -224,6 +165,40 @@ export class NodePublicCallsSimulator {
|
|
|
224
165
|
);
|
|
225
166
|
}
|
|
226
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Plans and prices the next block, and brings the world state up to the block that plan builds on. Retries
|
|
170
|
+
* once when the world state reaches the planned height with a different block: a prune between the archiver
|
|
171
|
+
* read and the sync leaves the two disagreeing, and forking anyway would simulate against state the plan's
|
|
172
|
+
* globals do not belong to. Any other sync failure propagates as is.
|
|
173
|
+
*/
|
|
174
|
+
private async prepareNextBlock(): Promise<PreparedNextBlock> {
|
|
175
|
+
for (let attempt = 0; attempt < MAX_PREDICTION_ATTEMPTS; attempt++) {
|
|
176
|
+
const { plan, globals } = await this.predictor.predict();
|
|
177
|
+
try {
|
|
178
|
+
const [, messages] = await Promise.all([
|
|
179
|
+
// Passing the hash makes the sync fork-aware: it waits for the block the plan builds on and throws if
|
|
180
|
+
// the world state ended up with a different block at that height.
|
|
181
|
+
this.worldStateSynchronizer.syncImmediate(plan.latestBlockNumber, BlockHash.fromString(plan.latestBlockHash)),
|
|
182
|
+
this.getNextCheckpointMessages(plan.newCheckpoint?.targetCheckpoint),
|
|
183
|
+
]);
|
|
184
|
+
return { plan, globals, messages };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (!isBlockHashMismatch(err)) {
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
this.log.warn(`World state disagrees with the planned next block, replanning`, {
|
|
190
|
+
blockNumber: plan.latestBlockNumber,
|
|
191
|
+
blockHash: plan.latestBlockHash,
|
|
192
|
+
error: err.message,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Cannot simulate public calls: world state and archiver disagree on the latest block (prune race), retry`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
227
202
|
/**
|
|
228
203
|
* Fetches the next checkpoint's L1-to-L2 messages to insert into the fork before simulation. Only set
|
|
229
204
|
* when opening a new checkpoint; when continuing an in-progress checkpoint the ongoing checkpoint's
|
|
@@ -254,130 +229,15 @@ export class NodePublicCallsSimulator {
|
|
|
254
229
|
return undefined;
|
|
255
230
|
}
|
|
256
231
|
}
|
|
232
|
+
}
|
|
257
233
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* opening a new checkpoint: the fork at `latestBlockNumber` already contains the ongoing checkpoint's
|
|
268
|
-
* L1-to-L2 messages, so inserting the next checkpoint's messages would append them a second time.
|
|
269
|
-
*/
|
|
270
|
-
private async copyGlobalVariablesFromLatestProposedBlock(
|
|
271
|
-
latestBlockNumber: BlockNumber,
|
|
272
|
-
blockNumber: BlockNumber,
|
|
273
|
-
): Promise<GlobalVariables> {
|
|
274
|
-
const latestBlockData = await this.blockSource.getBlockData({ number: latestBlockNumber });
|
|
275
|
-
if (!latestBlockData) {
|
|
276
|
-
throw new Error(
|
|
277
|
-
`Cannot simulate public calls: latest proposed block ${latestBlockNumber} has no header on this node ` +
|
|
278
|
-
`(torn archiver snapshot); retry`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
return GlobalVariables.from({ ...latestBlockData.header.globalVariables, blockNumber });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Opens a new checkpoint: the next block is the first of a fresh checkpoint. Picks the slot the next
|
|
286
|
-
* block will land in, mirroring the sequencer, and builds the same `SimulationOverridesPlan` the
|
|
287
|
-
* sequencer applies so the simulated mana min fee matches what the sequencer will write into the
|
|
288
|
-
* block header. Coinbase and fee recipient stay zero (we cannot know the future proposer's payout
|
|
289
|
-
* addresses), unlike continuing an in-progress checkpoint which inherits the real ones from the
|
|
290
|
-
* proposed header. Returns the target checkpoint so the caller inserts that checkpoint's L1-to-L2
|
|
291
|
-
* messages into the fork.
|
|
292
|
-
*/
|
|
293
|
-
private async buildGlobalVariablesForNewCheckpoint(
|
|
294
|
-
l2Tips: L2Tips,
|
|
295
|
-
proposedCheckpointData: ProposedCheckpointData | undefined,
|
|
296
|
-
blockNumber: BlockNumber,
|
|
297
|
-
): Promise<{ globalVariables: GlobalVariables; targetCheckpoint: CheckpointNumber }> {
|
|
298
|
-
const checkpointedCheckpointNumber = l2Tips.checkpointed.checkpoint.number;
|
|
299
|
-
// The new checkpoint sits on top of the proposed one when pipelining, otherwise on the
|
|
300
|
-
// checkpointed tip. The target slot and the overrides plan both derive from the single
|
|
301
|
-
// `proposedCheckpointData` read, so they cannot disagree about the proposed parent.
|
|
302
|
-
const proposedCheckpointNumber = proposedCheckpointData?.checkpointNumber ?? checkpointedCheckpointNumber;
|
|
303
|
-
|
|
304
|
-
const targetSlot = this.computeTargetSlot(proposedCheckpointData);
|
|
305
|
-
const plan = await this.buildSimulationOverridesPlan(proposedCheckpointData, checkpointedCheckpointNumber);
|
|
306
|
-
|
|
307
|
-
const checkpointGlobalVariables = await this.globalVariableBuilder.buildCheckpointGlobalVariables(
|
|
308
|
-
EthAddress.ZERO,
|
|
309
|
-
AztecAddress.ZERO,
|
|
310
|
-
targetSlot,
|
|
311
|
-
plan,
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
globalVariables: GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables }),
|
|
316
|
-
targetCheckpoint: CheckpointNumber(proposedCheckpointNumber + 1),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Slot the next block will land in. The first term is the sequencer's exact formula
|
|
322
|
-
* (`getEpochAndSlotInNextL1Slot().slot + PROPOSER_PIPELINING_SLOT_OFFSET`). The `max` with
|
|
323
|
-
* `proposedCheckpointSlot + 1` is an RPC-side approximation of the next build: when a proposed
|
|
324
|
-
* checkpoint is gossiped before its L1 slot starts, the next build (once its wall clock arrives)
|
|
325
|
-
* will target `parentSlot + 1`. The sequencer never advances its own target past wall clock — it
|
|
326
|
-
* just declines to build — so this is a prediction of inclusion globals, not literal sequencer
|
|
327
|
-
* behavior. The parent slot comes from the proposed checkpoint header so the slot and the
|
|
328
|
-
* overrides plan cannot derive from different snapshots.
|
|
329
|
-
*/
|
|
330
|
-
private computeTargetSlot(proposedCheckpointData: ProposedCheckpointData | undefined): SlotNumber {
|
|
331
|
-
const slotFromNextL1Timestamp =
|
|
332
|
-
this.epochCache.getEpochAndSlotInNextL1Slot().slot + PROPOSER_PIPELINING_SLOT_OFFSET;
|
|
333
|
-
const slotAfterProposedCheckpoint = proposedCheckpointData
|
|
334
|
-
? proposedCheckpointData.header.slotNumber + 1
|
|
335
|
-
: undefined;
|
|
336
|
-
return SlotNumber(Math.max(...compactArray([slotFromNextL1Timestamp, slotAfterProposedCheckpoint])));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Builds the chain-state overrides plan the simulator passes to `buildCheckpointGlobalVariables`,
|
|
341
|
-
* mirroring the sequencer (which always pins tips to neutralize prunes). When pipelining, the plan
|
|
342
|
-
* carries the proposed parent's archive, temp-checkpoint-log cell, and locally-derived fee header.
|
|
343
|
-
*
|
|
344
|
-
* Both the pipelining and invalid-pending-chain paths need a rollup contract for the L1 fee reads.
|
|
345
|
-
* Environments that omit it (e.g. TXE, which never has a proposed checkpoint and whose pending chain
|
|
346
|
-
* is always valid) fall back to pinning both pending and proven tips to the checkpointed tip, which
|
|
347
|
-
* neutralizes prunes in fee computation at the cost of non-pipelined fees.
|
|
348
|
-
*/
|
|
349
|
-
private async buildSimulationOverridesPlan(
|
|
350
|
-
proposedCheckpointData: ProposedCheckpointData | undefined,
|
|
351
|
-
checkpointedCheckpointNumber: CheckpointNumber,
|
|
352
|
-
): Promise<SimulationOverridesPlan | undefined> {
|
|
353
|
-
const rollup = this.rollupContract;
|
|
354
|
-
if (rollup) {
|
|
355
|
-
if (proposedCheckpointData) {
|
|
356
|
-
return buildCheckpointSimulationOverridesPlan({
|
|
357
|
-
checkpointNumber: CheckpointNumber(proposedCheckpointData.checkpointNumber + 1),
|
|
358
|
-
proposedCheckpointData,
|
|
359
|
-
checkpointedCheckpointNumber,
|
|
360
|
-
rollup,
|
|
361
|
-
signatureContext: this.signatureContext,
|
|
362
|
-
log: this.log,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const validationStatus = await this.blockSource.getPendingChainValidationStatus();
|
|
367
|
-
if (!validationStatus.valid) {
|
|
368
|
-
return buildCheckpointSimulationOverridesPlan({
|
|
369
|
-
checkpointNumber: CheckpointNumber(checkpointedCheckpointNumber + 1),
|
|
370
|
-
invalidateToPendingCheckpointNumber: CheckpointNumber(validationStatus.checkpoint.checkpointNumber - 1),
|
|
371
|
-
checkpointedCheckpointNumber,
|
|
372
|
-
rollup,
|
|
373
|
-
signatureContext: this.signatureContext,
|
|
374
|
-
log: this.log,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return new SimulationOverridesBuilder()
|
|
380
|
-
.withChainTips({ pending: checkpointedCheckpointNumber, proven: checkpointedCheckpointNumber })
|
|
381
|
-
.build();
|
|
382
|
-
}
|
|
234
|
+
/** The sync reached the planned height but found a different block there, so the chain moved under the plan. */
|
|
235
|
+
function isBlockHashMismatch(err: unknown): err is WorldStateSynchronizerError {
|
|
236
|
+
return (
|
|
237
|
+
isErrorClass(err, WorldStateSynchronizerError) &&
|
|
238
|
+
typeof err.cause === 'object' &&
|
|
239
|
+
err.cause !== null &&
|
|
240
|
+
'reason' in err.cause &&
|
|
241
|
+
err.cause.reason === 'block_hash_mismatch'
|
|
242
|
+
);
|
|
383
243
|
}
|