@aztec/ethereum 0.0.1-commit.04852196a → 0.0.1-commit.04d373f
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/client.d.ts +16 -2
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +27 -7
- package/dest/config.d.ts +6 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +5 -9
- package/dest/contracts/chain_state_override.d.ts +78 -0
- package/dest/contracts/chain_state_override.d.ts.map +1 -0
- package/dest/contracts/chain_state_override.js +137 -0
- package/dest/contracts/empire_base.d.ts +1 -3
- package/dest/contracts/empire_base.d.ts.map +1 -1
- package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
- package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
- package/dest/contracts/fee_asset_price_oracle.js +11 -6
- package/dest/contracts/governance.d.ts +108 -33
- package/dest/contracts/governance.d.ts.map +1 -1
- package/dest/contracts/governance.js +193 -12
- package/dest/contracts/governance_proposer.d.ts +33 -3
- package/dest/contracts/governance_proposer.d.ts.map +1 -1
- package/dest/contracts/governance_proposer.js +51 -9
- package/dest/contracts/inbox.d.ts +3 -3
- package/dest/contracts/inbox.d.ts.map +1 -1
- package/dest/contracts/inbox.js +12 -7
- package/dest/contracts/index.d.ts +3 -3
- package/dest/contracts/index.d.ts.map +1 -1
- package/dest/contracts/index.js +2 -2
- package/dest/contracts/multicall.d.ts +127 -12
- package/dest/contracts/multicall.d.ts.map +1 -1
- package/dest/contracts/multicall.js +201 -94
- package/dest/contracts/outbox.d.ts +21 -4
- package/dest/contracts/outbox.d.ts.map +1 -1
- package/dest/contracts/outbox.js +29 -6
- package/dest/contracts/rollup.d.ts +5254 -25
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +350 -67
- package/dest/contracts/{tally_slashing_proposer.d.ts → slashing_proposer.d.ts} +3 -4
- package/dest/contracts/slashing_proposer.d.ts.map +1 -0
- package/dest/contracts/{tally_slashing_proposer.js → slashing_proposer.js} +13 -15
- package/dest/deploy_aztec_l1_contracts.d.ts +3 -6
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +2 -4
- package/dest/generated/l1-contracts-defaults.d.ts +1 -1
- package/dest/generated/l1-contracts-defaults.js +1 -1
- package/dest/l1_artifacts.d.ts +13409 -15883
- package/dest/l1_artifacts.d.ts.map +1 -1
- package/dest/l1_artifacts.js +9 -24
- package/dest/l1_contract_addresses.d.ts +61 -65
- package/dest/l1_contract_addresses.d.ts.map +1 -1
- package/dest/l1_contract_addresses.js +101 -82
- package/dest/l1_reader.d.ts +5 -5
- package/dest/l1_reader.d.ts.map +1 -1
- package/dest/l1_reader.js +9 -7
- package/dest/l1_tx_utils/l1_tx_utils.d.ts +9 -1
- package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_tx_utils.js +54 -35
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.js +8 -4
- package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
- package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
- package/dest/l1_tx_utils/tx_delayer.js +7 -5
- package/dest/publisher_manager.d.ts +21 -7
- package/dest/publisher_manager.d.ts.map +1 -1
- package/dest/publisher_manager.js +81 -7
- package/dest/queries.d.ts +13 -2
- package/dest/queries.d.ts.map +1 -1
- package/dest/queries.js +62 -3
- package/dest/test/chain_monitor.d.ts +23 -3
- package/dest/test/chain_monitor.d.ts.map +1 -1
- package/dest/test/chain_monitor.js +38 -2
- package/dest/test/eth_cheat_codes.d.ts +12 -4
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +17 -4
- package/dest/test/rollup_cheat_codes.d.ts +31 -3
- package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
- package/dest/test/rollup_cheat_codes.js +59 -7
- package/dest/test/start_anvil.d.ts +15 -1
- package/dest/test/start_anvil.d.ts.map +1 -1
- package/dest/test/start_anvil.js +17 -2
- package/dest/utils.d.ts +8 -1
- package/dest/utils.d.ts.map +1 -1
- package/dest/utils.js +19 -0
- package/package.json +6 -6
- package/src/client.ts +28 -2
- package/src/config.ts +9 -13
- package/src/contracts/chain_state_override.ts +200 -0
- package/src/contracts/empire_base.ts +0 -2
- package/src/contracts/fee_asset_price_oracle.ts +10 -5
- package/src/contracts/governance.ts +266 -9
- package/src/contracts/governance_proposer.ts +52 -5
- package/src/contracts/inbox.ts +13 -5
- package/src/contracts/index.ts +2 -2
- package/src/contracts/multicall.ts +243 -105
- package/src/contracts/outbox.ts +38 -8
- package/src/contracts/rollup.ts +391 -73
- package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +14 -16
- package/src/deploy_aztec_l1_contracts.ts +1 -5
- package/src/generated/l1-contracts-defaults.ts +1 -1
- package/src/l1_artifacts.ts +12 -35
- package/src/l1_contract_addresses.ts +120 -94
- package/src/l1_reader.ts +17 -10
- package/src/l1_tx_utils/l1_tx_utils.ts +51 -28
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +8 -4
- package/src/l1_tx_utils/tx_delayer.ts +16 -7
- package/src/publisher_manager.ts +105 -10
- package/src/queries.ts +70 -4
- package/src/test/chain_monitor.ts +65 -3
- package/src/test/eth_cheat_codes.ts +16 -4
- package/src/test/rollup_cheat_codes.ts +64 -6
- package/src/test/start_anvil.ts +33 -2
- package/src/utils.ts +18 -0
- package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
- package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
- package/dest/contracts/empire_slashing_proposer.js +0 -216
- package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
- package/src/contracts/empire_slashing_proposer.ts +0 -265
|
@@ -130,9 +130,10 @@ export class ReadOnlyL1TxUtils {
|
|
|
130
130
|
const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS);
|
|
131
131
|
for (let i = 0; i < numBlocks; i++) {
|
|
132
132
|
// each block can go up 12.5% from previous baseFee
|
|
133
|
-
|
|
133
|
+
// ceil, (a+b-1)/b, to avoid truncation at small values (e.g. 1 wei blob base fee)
|
|
134
|
+
maxFeePerGas = (maxFeePerGas * (1_000n + 125n) + 999n) / 1_000n;
|
|
134
135
|
// same for blob gas fee
|
|
135
|
-
maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n)) / 1_000n;
|
|
136
|
+
maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n) + 999n) / 1_000n;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
if (attempt > 0) {
|
|
@@ -242,13 +243,16 @@ export class ReadOnlyL1TxUtils {
|
|
|
242
243
|
const gasConfig = { ...this.config, ..._gasConfig };
|
|
243
244
|
let initialEstimate = 0n;
|
|
244
245
|
if (_blobInputs) {
|
|
245
|
-
// @note requests with blobs also require maxFeePerBlobGas to be set
|
|
246
|
+
// @note requests with blobs also require maxFeePerBlobGas to be set.
|
|
247
|
+
// Use 2x buffer for maxFeePerBlobGas to avoid stale fees and to pass EIP-4844 validation (even if it is a gas estimation call).
|
|
248
|
+
// 1. maxFeePerBlobGas >= blobBaseFee
|
|
249
|
+
// 2. account balance >= gas * maxFeePerGas + maxFeePerBlobGas * blobCount + value
|
|
246
250
|
const gasPrice = await this.getGasPrice(gasConfig, true, 0);
|
|
247
251
|
initialEstimate = await this.client.estimateGas({
|
|
248
252
|
account,
|
|
249
253
|
...request,
|
|
250
254
|
..._blobInputs,
|
|
251
|
-
maxFeePerBlobGas: gasPrice.maxFeePerBlobGas
|
|
255
|
+
maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! * 2n,
|
|
252
256
|
gas: MAX_L1_TX_LIMIT,
|
|
253
257
|
blockTag: 'latest',
|
|
254
258
|
});
|
|
@@ -81,7 +81,11 @@ export class Delayer {
|
|
|
81
81
|
|
|
82
82
|
public maxInclusionTimeIntoSlot: number | undefined = undefined;
|
|
83
83
|
public ethereumSlotDuration: bigint;
|
|
84
|
-
public nextWait:
|
|
84
|
+
public nextWait:
|
|
85
|
+
| { l1Timestamp: bigint; timeoutSeconds?: number }
|
|
86
|
+
| { l1BlockNumber: bigint; timeoutSeconds?: number }
|
|
87
|
+
| { indefinitely: true }
|
|
88
|
+
| undefined = undefined;
|
|
85
89
|
public sentTxHashes: Hex[] = [];
|
|
86
90
|
public cancelledTxs: Hex[] = [];
|
|
87
91
|
|
|
@@ -110,13 +114,13 @@ export class Delayer {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/** Delays the next tx to be sent so it lands on the given L1 block number. */
|
|
113
|
-
pauseNextTxUntilBlock(l1BlockNumber: number | bigint) {
|
|
114
|
-
this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber) };
|
|
117
|
+
pauseNextTxUntilBlock(l1BlockNumber: number | bigint, timeoutSeconds?: number) {
|
|
118
|
+
this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber), timeoutSeconds };
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
/** Delays the next tx to be sent so it lands on the given timestamp. */
|
|
118
|
-
pauseNextTxUntilTimestamp(l1Timestamp: number | bigint) {
|
|
119
|
-
this.nextWait = { l1Timestamp: BigInt(l1Timestamp) };
|
|
122
|
+
pauseNextTxUntilTimestamp(l1Timestamp: number | bigint, timeoutSeconds?: number) {
|
|
123
|
+
this.nextWait = { l1Timestamp: BigInt(l1Timestamp), timeoutSeconds };
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/** Delays the next tx to be sent indefinitely. */
|
|
@@ -198,9 +202,14 @@ export function wrapClientWithDelayer<T extends ViemClient>(client: T, delayer:
|
|
|
198
202
|
// Or wait until the desired block number or timestamp
|
|
199
203
|
wait =
|
|
200
204
|
'l1BlockNumber' in waitUntil
|
|
201
|
-
? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger)
|
|
205
|
+
? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger, waitUntil.timeoutSeconds)
|
|
202
206
|
: 'l1Timestamp' in waitUntil
|
|
203
|
-
? waitUntilL1Timestamp(
|
|
207
|
+
? waitUntilL1Timestamp(
|
|
208
|
+
publicClient,
|
|
209
|
+
waitUntil.l1Timestamp - delayer.ethereumSlotDuration,
|
|
210
|
+
logger,
|
|
211
|
+
waitUntil.timeoutSeconds,
|
|
212
|
+
)
|
|
204
213
|
: undefined;
|
|
205
214
|
|
|
206
215
|
logger.info(`Delaying tx ${txHash} until ${inspect(waitUntil)}`, {
|
package/src/publisher_manager.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { pick } from '@aztec/foundation/collection';
|
|
2
2
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
3
4
|
|
|
5
|
+
import { Multicall3 } from './contracts/multicall.js';
|
|
4
6
|
import { L1TxUtils, TxUtilsState } from './l1_tx_utils/index.js';
|
|
5
7
|
|
|
6
8
|
// Defines the order in which we prioritise publishers based on their state (first is better)
|
|
@@ -27,24 +29,72 @@ const busyStates: TxUtilsState[] = [
|
|
|
27
29
|
|
|
28
30
|
export type PublisherFilter<UtilsType extends L1TxUtils> = (utils: UtilsType) => boolean;
|
|
29
31
|
|
|
32
|
+
/** Config accepted by PublisherManager. */
|
|
33
|
+
type PublisherManagerConfig = {
|
|
34
|
+
publisherAllowInvalidStates?: boolean;
|
|
35
|
+
publisherFundingThreshold?: bigint;
|
|
36
|
+
publisherFundingAmount?: bigint;
|
|
37
|
+
};
|
|
38
|
+
|
|
30
39
|
export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
|
|
31
40
|
private log: Logger;
|
|
32
|
-
private config:
|
|
41
|
+
private config: PublisherManagerConfig;
|
|
42
|
+
private static readonly FUNDING_CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
|
43
|
+
private funder?: UtilsType;
|
|
44
|
+
private fundingPromise?: RunningPromise;
|
|
33
45
|
|
|
34
46
|
constructor(
|
|
35
47
|
private publishers: UtilsType[],
|
|
36
|
-
config:
|
|
37
|
-
bindings?: LoggerBindings,
|
|
48
|
+
config: PublisherManagerConfig,
|
|
49
|
+
opts?: { bindings?: LoggerBindings; funder?: UtilsType },
|
|
38
50
|
) {
|
|
39
|
-
this.
|
|
51
|
+
this.funder = opts?.funder;
|
|
52
|
+
this.log = createLogger('publisher:manager', opts?.bindings);
|
|
40
53
|
this.log.info(`PublisherManager initialized with ${publishers.length} publishers.`);
|
|
41
54
|
this.publishers = publishers;
|
|
42
|
-
this.config = pick(config, 'publisherAllowInvalidStates');
|
|
55
|
+
this.config = pick(config, 'publisherAllowInvalidStates', 'publisherFundingThreshold', 'publisherFundingAmount');
|
|
56
|
+
|
|
57
|
+
const hasThreshold = this.config.publisherFundingThreshold !== undefined;
|
|
58
|
+
const hasAmount = this.config.publisherFundingAmount !== undefined;
|
|
59
|
+
if (hasThreshold !== hasAmount) {
|
|
60
|
+
this.log.warn(`Incomplete funding config: both publisherFundingThreshold and publisherFundingAmount must be set`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.funder) {
|
|
64
|
+
const funderAddress = this.funder.getSenderAddress();
|
|
65
|
+
if (publishers.some(p => p.getSenderAddress().equals(funderAddress))) {
|
|
66
|
+
this.log.error(`Funding account ${funderAddress} is also a publisher, disabling funding to avoid self-funding`);
|
|
67
|
+
this.funder = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
43
70
|
}
|
|
44
71
|
|
|
45
|
-
/** Loads the state of all publishers and
|
|
46
|
-
public async
|
|
47
|
-
await Promise.all(
|
|
72
|
+
/** Loads the state of all publishers and the funder, and starts periodic funding checks. */
|
|
73
|
+
public async start(): Promise<void> {
|
|
74
|
+
await Promise.all([
|
|
75
|
+
...this.publishers.map(pub => pub.loadStateAndResumeMonitoring()),
|
|
76
|
+
this.funder?.loadStateAndResumeMonitoring(),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
this.funder &&
|
|
81
|
+
this.config.publisherFundingThreshold !== undefined &&
|
|
82
|
+
this.config.publisherFundingAmount !== undefined
|
|
83
|
+
) {
|
|
84
|
+
this.fundingPromise = new RunningPromise(
|
|
85
|
+
() => this.triggerFundingIfNeeded(),
|
|
86
|
+
this.log,
|
|
87
|
+
PublisherManager.FUNDING_CHECK_INTERVAL_MS,
|
|
88
|
+
);
|
|
89
|
+
this.fundingPromise.start();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Stops the funding loop and interrupts all publishers. */
|
|
94
|
+
public async stop(): Promise<void> {
|
|
95
|
+
await this.fundingPromise?.stop();
|
|
96
|
+
this.publishers.forEach(pub => pub.interrupt());
|
|
97
|
+
this.funder?.interrupt();
|
|
48
98
|
}
|
|
49
99
|
|
|
50
100
|
// Finds and prioritises available publishers based on
|
|
@@ -102,7 +152,52 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
|
|
|
102
152
|
return sortedPublishers[0].publisher;
|
|
103
153
|
}
|
|
104
154
|
|
|
105
|
-
|
|
106
|
-
|
|
155
|
+
/** Check all publisher balances and fund those below threshold. */
|
|
156
|
+
private async triggerFundingIfNeeded(): Promise<void> {
|
|
157
|
+
const { funder, config } = this;
|
|
158
|
+
if (!funder || config.publisherFundingThreshold === undefined || config.publisherFundingAmount === undefined) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const allBalances = await Promise.all(
|
|
163
|
+
this.publishers.map(async pub => ({ balance: await pub.getSenderBalance(), publisher: pub })),
|
|
164
|
+
);
|
|
165
|
+
const lowBalance = allBalances.filter(p => p.balance < config.publisherFundingThreshold!);
|
|
166
|
+
if (lowBalance.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const fundingAmount = config.publisherFundingAmount!;
|
|
171
|
+
const funderBalance = await funder.getSenderBalance();
|
|
172
|
+
|
|
173
|
+
if (funderBalance < 10n * fundingAmount) {
|
|
174
|
+
this.log.warn(`Funding account balance is low`, { funderBalance, threshold: 10n * fundingAmount });
|
|
175
|
+
}
|
|
176
|
+
const affordableCount = Number(funderBalance / fundingAmount);
|
|
177
|
+
if (affordableCount === 0) {
|
|
178
|
+
this.log.error(`Funding account balance too low to fund any publisher`, { funderBalance, fundingAmount });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (affordableCount < lowBalance.length) {
|
|
182
|
+
this.log.warn(`Funder can only afford ${affordableCount}/${lowBalance.length} publishers`, {
|
|
183
|
+
funderBalance,
|
|
184
|
+
fundingAmount,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const toFund = lowBalance.slice(0, affordableCount).map(p => p.publisher);
|
|
189
|
+
await this.fundPublishers(toFund);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Fund publishers via a single Multicall3 aggregate3Value transaction. */
|
|
193
|
+
private async fundPublishers(publishers: UtilsType[]): Promise<void> {
|
|
194
|
+
const fundingAmount = this.config.publisherFundingAmount!;
|
|
195
|
+
const calls = publishers.map(pub => ({
|
|
196
|
+
to: pub.getSenderAddress().toString(),
|
|
197
|
+
value: fundingAmount,
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
await Multicall3.forwardValue(calls, this.funder!, this.log);
|
|
201
|
+
this.log.info(`Funded ${publishers.length} publishers`);
|
|
107
202
|
}
|
|
108
203
|
}
|
package/src/queries.ts
CHANGED
|
@@ -1,11 +1,77 @@
|
|
|
1
1
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
2
2
|
|
|
3
|
+
import { BaseError, type Block } from 'viem';
|
|
4
|
+
|
|
3
5
|
import { DefaultL1ContractsConfig, type L1ContractsConfig } from './config.js';
|
|
4
6
|
import { ReadOnlyGovernanceContract } from './contracts/governance.js';
|
|
5
7
|
import { GovernanceProposerContract } from './contracts/governance_proposer.js';
|
|
6
8
|
import { InboxContract } from './contracts/inbox.js';
|
|
7
9
|
import { RollupContract } from './contracts/rollup.js';
|
|
8
|
-
import type { ViemPublicClient } from './types.js';
|
|
10
|
+
import type { ViemClient, ViemPublicClient } from './types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the L1 finalized block, or `undefined` if the chain does not yet have one
|
|
14
|
+
* (common on freshly started devnets). Rethrows any other RPC error.
|
|
15
|
+
*/
|
|
16
|
+
export async function getFinalizedL1Block(client: ViemClient): Promise<Block<bigint, false, 'finalized'> | undefined> {
|
|
17
|
+
try {
|
|
18
|
+
return await client.getBlock({ blockTag: 'finalized', includeTransactions: false });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (isFinalizedBlockTagNotFoundError(err)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Error messages returned by popular Ethereum execution clients when
|
|
28
|
+
// eth_getBlockByNumber / eth_call is called with blockTag "finalized" or
|
|
29
|
+
// "safe" and no such block has been produced yet:
|
|
30
|
+
//
|
|
31
|
+
// geth "finalized block not found"
|
|
32
|
+
// "safe block not found"
|
|
33
|
+
// reth "block not found: finalized"
|
|
34
|
+
// "block not found: safe"
|
|
35
|
+
// nethermind "Unknown block error" (same for both tags)
|
|
36
|
+
// besu "Unknown block"
|
|
37
|
+
// erigon 'block "finalized" not available (head block: N)'
|
|
38
|
+
// 'block "safe" not available (head block: N)'
|
|
39
|
+
//
|
|
40
|
+
// A combined regex covers all five:
|
|
41
|
+
const FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE =
|
|
42
|
+
/(finalized|safe) block not found|block not found: (finalized|safe)|unknown block|block "(finalized|safe)" not available/i;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns true if the error originates from an RPC call that failed because
|
|
46
|
+
* the "finalized" (or "safe") block tag is not yet available on the chain.
|
|
47
|
+
*/
|
|
48
|
+
export function isFinalizedBlockTagNotFoundError(err: unknown): boolean {
|
|
49
|
+
if (!err) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (err instanceof BaseError) {
|
|
54
|
+
const hit = err.walk((e: any) => matchesFinalizedBlockTagNotFound(e));
|
|
55
|
+
if (hit) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let cur: any = err, i = 0; cur && i < 10; cur = cur.cause, i++) {
|
|
61
|
+
if (matchesFinalizedBlockTagNotFound(cur)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function matchesFinalizedBlockTagNotFound(e: any): boolean {
|
|
69
|
+
if (!e) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const text = `${e.details ?? ''} ${e.message ?? ''} ${e.shortMessage ?? ''}`;
|
|
73
|
+
return FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE.test(text);
|
|
74
|
+
}
|
|
9
75
|
|
|
10
76
|
/** Reads the L1ContractsConfig from L1 contracts. */
|
|
11
77
|
export async function getL1ContractsConfig(
|
|
@@ -76,8 +142,8 @@ export async function getL1ContractsConfig(
|
|
|
76
142
|
slasherProposer?.getRoundSize() ?? 0n,
|
|
77
143
|
slasherProposer?.getLifetimeInRounds() ?? 0n,
|
|
78
144
|
slasherProposer?.getExecutionDelayInRounds() ?? 0n,
|
|
79
|
-
slasherProposer?.
|
|
80
|
-
slasherProposer?.
|
|
145
|
+
slasherProposer?.getSlashOffsetInRounds() ?? 0n,
|
|
146
|
+
slasherProposer?.getSlashingAmounts() ?? [0n, 0n, 0n],
|
|
81
147
|
slasher?.getVetoer() ?? EthAddress.ZERO,
|
|
82
148
|
slasher?.getSlashingDisableDuration() ?? 0,
|
|
83
149
|
rollup.getManaTarget(),
|
|
@@ -114,7 +180,7 @@ export async function getL1ContractsConfig(
|
|
|
114
180
|
rollupVersion: Number(rollupVersion),
|
|
115
181
|
genesisArchiveTreeRoot: genesisArchiveTreeRoot.toString(),
|
|
116
182
|
exitDelaySeconds: Number(exitDelay),
|
|
117
|
-
|
|
183
|
+
slasherEnabled: !!slasherProposer,
|
|
118
184
|
slashingOffsetInRounds: Number(slashingOffsetInRounds),
|
|
119
185
|
slashAmountSmall: slashingAmounts[0],
|
|
120
186
|
slashAmountMedium: slashingAmounts[1],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RollupContract } from '@aztec/ethereum/contracts';
|
|
1
|
+
import type { ManaMinFeeComponents, RollupContract } from '@aztec/ethereum/contracts';
|
|
2
2
|
import { InboxContract } from '@aztec/ethereum/contracts';
|
|
3
3
|
import { CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
4
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -10,6 +10,20 @@ import { EventEmitter } from 'events';
|
|
|
10
10
|
|
|
11
11
|
import type { ViemClient } from '../types.js';
|
|
12
12
|
|
|
13
|
+
/** L2 fee data reported by the chain monitor. */
|
|
14
|
+
export type L2FeeData = ManaMinFeeComponents & {
|
|
15
|
+
/** Total minimum fee per mana in Fee Juice (sum of sequencerCost + proverCost + congestionCost). */
|
|
16
|
+
minFeePerMana: bigint;
|
|
17
|
+
/** L1 base fee observed by the oracle. */
|
|
18
|
+
l1BaseFee: bigint;
|
|
19
|
+
/** L1 blob fee observed by the oracle. */
|
|
20
|
+
l1BlobFee: bigint;
|
|
21
|
+
/** ETH per fee asset exchange rate (1e12 precision). */
|
|
22
|
+
ethPerFeeAsset: bigint;
|
|
23
|
+
/** Mana target per checkpoint. */
|
|
24
|
+
manaTarget: bigint;
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
export type ChainMonitorEventMap = {
|
|
14
28
|
'l1-block': [{ l1BlockNumber: number; timestamp: bigint }];
|
|
15
29
|
checkpoint: [
|
|
@@ -19,6 +33,7 @@ export type ChainMonitorEventMap = {
|
|
|
19
33
|
'l2-messages': [{ totalL2Messages: number; l1BlockNumber: number }];
|
|
20
34
|
'l2-epoch': [{ l2EpochNumber: EpochNumber; timestamp: bigint; committee: EthAddress[] | undefined }];
|
|
21
35
|
'l2-slot': [{ l2SlotNumber: SlotNumber; timestamp: bigint }];
|
|
36
|
+
'l2-fees': [L2FeeData];
|
|
22
37
|
};
|
|
23
38
|
|
|
24
39
|
/** Utility class that polls the chain on quick intervals and logs new L1 blocks, L2 blocks, and L2 proofs. */
|
|
@@ -45,6 +60,8 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
|
|
|
45
60
|
public l2EpochNumber!: EpochNumber;
|
|
46
61
|
/** Current L2 slot number */
|
|
47
62
|
public l2SlotNumber!: SlotNumber;
|
|
63
|
+
/** Current L2 fee data (components of the minimum fee per mana). */
|
|
64
|
+
public l2FeeData!: L2FeeData;
|
|
48
65
|
|
|
49
66
|
constructor(
|
|
50
67
|
private readonly rollup: RollupContract,
|
|
@@ -77,7 +94,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
|
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
protected async getInbox() {
|
|
81
98
|
if (!this.inbox) {
|
|
82
99
|
const { inboxAddress } = await this.rollup.getRollupAddresses();
|
|
83
100
|
this.inbox = new InboxContract(this.l1Client, inboxAddress);
|
|
@@ -158,7 +175,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
|
|
|
158
175
|
this.l2EpochNumber = l2Epoch;
|
|
159
176
|
committee = await this.rollup.getCurrentEpochCommittee();
|
|
160
177
|
this.emit('l2-epoch', { l2EpochNumber: l2Epoch, timestamp, committee });
|
|
161
|
-
msg += ` starting new epoch ${this.l2EpochNumber}
|
|
178
|
+
msg += ` starting new epoch ${this.l2EpochNumber}`;
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
if (l2SlotNumber !== this.l2SlotNumber) {
|
|
@@ -166,6 +183,13 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
|
|
|
166
183
|
this.emit('l2-slot', { l2SlotNumber, timestamp });
|
|
167
184
|
}
|
|
168
185
|
|
|
186
|
+
const feeData = await this.fetchFeeData(timestamp);
|
|
187
|
+
if (this.hasFeeDataChanged(feeData)) {
|
|
188
|
+
msg += ` with L2 min fee ${feeData.minFeePerMana}`;
|
|
189
|
+
this.l2FeeData = feeData;
|
|
190
|
+
this.emit('l2-fees', feeData);
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
this.logger.info(msg, {
|
|
170
194
|
currentTimestamp: this.dateProvider.nowInSeconds(),
|
|
171
195
|
l1Timestamp: timestamp,
|
|
@@ -176,6 +200,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
|
|
|
176
200
|
provenCheckpointNumber: this.provenCheckpointNumber,
|
|
177
201
|
totalL2Messages: this.totalL2Messages,
|
|
178
202
|
committee,
|
|
203
|
+
...this.l2FeeData,
|
|
179
204
|
});
|
|
180
205
|
|
|
181
206
|
return this;
|
|
@@ -196,6 +221,11 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
|
|
|
196
221
|
});
|
|
197
222
|
}
|
|
198
223
|
|
|
224
|
+
public async waitUntilNextL2Slot(): Promise<void> {
|
|
225
|
+
const targetSlot = SlotNumber.add((await this.run()).l2SlotNumber, 1);
|
|
226
|
+
return this.waitUntilL2Slot(targetSlot);
|
|
227
|
+
}
|
|
228
|
+
|
|
199
229
|
public waitUntilL1Block(block: number | bigint): Promise<void> {
|
|
200
230
|
const targetBlock = typeof block === 'bigint' ? block.valueOf() : block;
|
|
201
231
|
if (this.l1BlockNumber >= targetBlock) {
|
|
@@ -242,4 +272,36 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
|
|
|
242
272
|
this.on('checkpoint', listener);
|
|
243
273
|
});
|
|
244
274
|
}
|
|
275
|
+
|
|
276
|
+
private async fetchFeeData(timestamp: bigint): Promise<L2FeeData> {
|
|
277
|
+
const [components, minFeePerMana, l1Fees, ethPerFeeAsset, manaTarget] = await Promise.all([
|
|
278
|
+
this.rollup.getManaMinFeeComponentsAt(timestamp, true),
|
|
279
|
+
this.rollup.getManaMinFeeAt(timestamp, true),
|
|
280
|
+
this.rollup.getL1FeesAt(timestamp),
|
|
281
|
+
this.rollup.getEthPerFeeAsset(),
|
|
282
|
+
this.rollup.getManaTarget(),
|
|
283
|
+
]);
|
|
284
|
+
return {
|
|
285
|
+
...components,
|
|
286
|
+
minFeePerMana,
|
|
287
|
+
l1BaseFee: l1Fees.baseFee,
|
|
288
|
+
l1BlobFee: l1Fees.blobFee,
|
|
289
|
+
ethPerFeeAsset,
|
|
290
|
+
manaTarget,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private hasFeeDataChanged(newData: L2FeeData): boolean {
|
|
295
|
+
if (!this.l2FeeData) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return (
|
|
299
|
+
this.l2FeeData.sequencerCost !== newData.sequencerCost ||
|
|
300
|
+
this.l2FeeData.proverCost !== newData.proverCost ||
|
|
301
|
+
this.l2FeeData.congestionCost !== newData.congestionCost ||
|
|
302
|
+
this.l2FeeData.l1BaseFee !== newData.l1BaseFee ||
|
|
303
|
+
this.l2FeeData.l1BlobFee !== newData.l1BlobFee ||
|
|
304
|
+
this.l2FeeData.ethPerFeeAsset !== newData.ethPerFeeAsset
|
|
305
|
+
);
|
|
306
|
+
}
|
|
245
307
|
}
|
|
@@ -85,14 +85,26 @@ export class EthCheatCodes {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* Get the
|
|
89
|
-
*
|
|
88
|
+
* Get the timestamp of the latest mined L1 block.
|
|
89
|
+
* Note: this is NOT the current time — it's the discrete timestamp of the last block.
|
|
90
|
+
* Between blocks, the actual chain time advances but no new block reflects it.
|
|
91
|
+
* @returns The latest block timestamp in seconds
|
|
90
92
|
*/
|
|
91
|
-
public async
|
|
93
|
+
public async lastBlockTimestamp(): Promise<number> {
|
|
92
94
|
const res = await this.doRpcCall('eth_getBlockByNumber', ['latest', true]);
|
|
93
95
|
return parseInt(res.timestamp, 16);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Get the timestamp of the pending L1 block — the block that anvil will mine next.
|
|
100
|
+
* Reflects any prior `setNextBlockTimestamp` call or the configured block interval.
|
|
101
|
+
* @returns The pending block timestamp in seconds.
|
|
102
|
+
*/
|
|
103
|
+
public async nextBlockTimestamp(): Promise<number> {
|
|
104
|
+
const res = await this.doRpcCall('eth_getBlockByNumber', ['pending', false]);
|
|
105
|
+
return parseInt(res.timestamp, 16);
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
/**
|
|
97
109
|
* Advance the chain by a number of blocks
|
|
98
110
|
* @param numberOfBlocks - The number of blocks to mine
|
|
@@ -552,7 +564,7 @@ export class EthCheatCodes {
|
|
|
552
564
|
}
|
|
553
565
|
|
|
554
566
|
public async syncDateProvider() {
|
|
555
|
-
const timestamp = await this.
|
|
567
|
+
const timestamp = await this.lastBlockTimestamp();
|
|
556
568
|
if ('setTime' in this.dateProvider) {
|
|
557
569
|
this.dateProvider.setTime(timestamp * 1000);
|
|
558
570
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getContract,
|
|
16
16
|
hexToBigInt,
|
|
17
17
|
http,
|
|
18
|
+
keccak256,
|
|
18
19
|
} from 'viem';
|
|
19
20
|
|
|
20
21
|
import { EthCheatCodes } from './eth_cheat_codes.js';
|
|
@@ -53,7 +54,17 @@ export class RollupCheatCodes {
|
|
|
53
54
|
/** Returns the current slot */
|
|
54
55
|
public async getSlot(): Promise<SlotNumber> {
|
|
55
56
|
const ts = BigInt((await this.client.getBlock()).timestamp);
|
|
56
|
-
return
|
|
57
|
+
return this.getSlotAt(ts);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Returns the slot number at a given timestamp. */
|
|
61
|
+
public async getSlotAt(timestamp: bigint): Promise<SlotNumber> {
|
|
62
|
+
return SlotNumber.fromBigInt(await this.rollup.read.getSlotAt([timestamp]));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Returns the timestamp for the start of a given slot. */
|
|
66
|
+
public async getTimestampForSlot(slot: SlotNumber): Promise<bigint> {
|
|
67
|
+
return await this.rollup.read.getTimestampForSlot([BigInt(slot)]);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/** Returns the number of seconds until the start of the given slot based on L1 block timestamp. */
|
|
@@ -168,6 +179,21 @@ export class RollupCheatCodes {
|
|
|
168
179
|
return [timestamp, nextSlot];
|
|
169
180
|
}
|
|
170
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Warps L1 time to the start of an explicit absolute slot. Unlike `advanceSlots(N)` which is
|
|
184
|
+
* relative to the current L1 timestamp at call time and therefore races with real-time
|
|
185
|
+
* progression between query and warp, this lands at the slot regardless of latency.
|
|
186
|
+
*
|
|
187
|
+
* Throws if the requested slot is in the past relative to current L1 time (anvil cannot
|
|
188
|
+
* rewind).
|
|
189
|
+
*/
|
|
190
|
+
public async advanceToSlot(slot: SlotNumber) {
|
|
191
|
+
const timestamp = await this.rollup.read.getTimestampForSlot([BigInt(slot)]);
|
|
192
|
+
await this.ethCheatCodes.warp(Number(timestamp), { silent: true, resetBlockInterval: true });
|
|
193
|
+
this.logger.warn(`Advanced to slot ${slot}`, { timestamp });
|
|
194
|
+
return timestamp;
|
|
195
|
+
}
|
|
196
|
+
|
|
171
197
|
/**
|
|
172
198
|
* Warps time in L1 equivalent to however many slots.
|
|
173
199
|
* @param howMany - The number of slots to advance.
|
|
@@ -259,12 +285,14 @@ export class RollupCheatCodes {
|
|
|
259
285
|
});
|
|
260
286
|
}
|
|
261
287
|
|
|
262
|
-
public insertOutbox(epoch: EpochNumber, outHash: bigint) {
|
|
288
|
+
public insertOutbox(epoch: EpochNumber, numCheckpointsInEpoch: number, outHash: bigint) {
|
|
263
289
|
return this.ethCheatCodes.execWithPausedAnvil(async () => {
|
|
264
290
|
const outboxAddress = await this.rollup.read.getOutbox();
|
|
265
|
-
const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch);
|
|
291
|
+
const epochRootSlot = OutboxContract.getEpochRootStorageSlot(epoch, numCheckpointsInEpoch);
|
|
266
292
|
await this.ethCheatCodes.store(EthAddress.fromString(outboxAddress), epochRootSlot, outHash);
|
|
267
|
-
this.logger.warn(
|
|
293
|
+
this.logger.warn(
|
|
294
|
+
`Advanced outbox to epoch ${epoch} numCheckpointsInEpoch ${numCheckpointsInEpoch} with out hash ${outHash}`,
|
|
295
|
+
);
|
|
268
296
|
});
|
|
269
297
|
}
|
|
270
298
|
|
|
@@ -313,7 +341,8 @@ export class RollupCheatCodes {
|
|
|
313
341
|
}
|
|
314
342
|
|
|
315
343
|
/**
|
|
316
|
-
* Directly updates proving cost per mana.
|
|
344
|
+
* Directly updates proving cost per mana. Throws if the on-chain tx reverts
|
|
345
|
+
* (e.g. rate-limit cooldown, step cap, or floor) instead of silently succeeding.
|
|
317
346
|
* @param ethValue - The new proving cost per mana in ETH
|
|
318
347
|
*/
|
|
319
348
|
public async setProvingCostPerMana(ethValue: bigint) {
|
|
@@ -323,8 +352,37 @@ export class RollupCheatCodes {
|
|
|
323
352
|
chain: this.client.chain,
|
|
324
353
|
gasLimit: 1000000n,
|
|
325
354
|
});
|
|
326
|
-
await this.client.waitForTransactionReceipt({ hash });
|
|
355
|
+
const receipt = await this.client.waitForTransactionReceipt({ hash });
|
|
356
|
+
if (receipt.status !== 'success') {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`setProvingCostPerMana(${ethValue}) reverted on L1 (tx ${hash}). ` +
|
|
359
|
+
`Likely FeeLib rate-limit (30-day cooldown or 1.5x step cap); ` +
|
|
360
|
+
`use clearProvingCostCooldown() between successive updates.`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
327
363
|
this.logger.warn(`Updated proving cost per mana to ${ethValue}`);
|
|
328
364
|
});
|
|
329
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Resets the 30-day proving-cost update cooldown enforced by FeeLib.updateProvingCostPerMana
|
|
369
|
+
* by zeroing `FeeStore.provingCostLastUpdate` directly in contract storage. Use between
|
|
370
|
+
* successive setProvingCostPerMana / bumpProvingCostPerMana calls so the later update can
|
|
371
|
+
* land instead of reverting. Does not touch L1 time, so PXE/tx-expiration state stays intact.
|
|
372
|
+
*
|
|
373
|
+
* @note This is tightly coupled to the `FeeStore` layout in
|
|
374
|
+
* l1-contracts/src/core/libraries/rollup/FeeLib.sol:
|
|
375
|
+
* slot + 0: CompressedFeeConfig config (uint256)
|
|
376
|
+
* slot + 1: L1GasOracleValues l1GasOracleValues (14+14+4 bytes, packed)
|
|
377
|
+
* slot + 2: uint64 provingCostLastUpdate (only member — zeroing the slot is safe)
|
|
378
|
+
* If the struct layout changes, update the offset below.
|
|
379
|
+
*/
|
|
380
|
+
public async clearProvingCostCooldown() {
|
|
381
|
+
const feeStoreBaseSlot = hexToBigInt(keccak256(Buffer.from('aztec.fee.storage', 'utf-8')));
|
|
382
|
+
const provingCostLastUpdateSlot = feeStoreBaseSlot + 2n;
|
|
383
|
+
await this.ethCheatCodes.store(EthAddress.fromString(this.rollup.address), provingCostLastUpdateSlot, 0n, {
|
|
384
|
+
silent: true,
|
|
385
|
+
});
|
|
386
|
+
this.logger.warn(`Cleared proving-cost update cooldown`);
|
|
387
|
+
}
|
|
330
388
|
}
|
package/src/test/start_anvil.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
2
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
3
|
+
import type { TestDateProvider } from '@aztec/foundation/timer';
|
|
3
4
|
import { fileURLToPath } from '@aztec/foundation/url';
|
|
4
5
|
|
|
5
6
|
import { type ChildProcess, spawn } from 'child_process';
|
|
@@ -26,6 +27,19 @@ export async function startAnvil(
|
|
|
26
27
|
chainId?: number;
|
|
27
28
|
/** The hardfork to use (e.g. 'cancun', 'latest'). */
|
|
28
29
|
hardfork?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Number of slots per epoch used by anvil to compute the 'finalized' and 'safe' block tags.
|
|
32
|
+
* Anvil reports `finalized = latest - slotsInAnEpoch * 2`.
|
|
33
|
+
* Defaults to 1 so the finalized block advances immediately, making tests that check
|
|
34
|
+
* L1-finality-based logic work without needing hundreds of mined blocks.
|
|
35
|
+
*/
|
|
36
|
+
slotsInAnEpoch?: number;
|
|
37
|
+
/**
|
|
38
|
+
* If provided, the date provider will be synced to anvil's block time on every mined block.
|
|
39
|
+
* This keeps the dateProvider in lockstep with anvil's chain time, avoiding drift between
|
|
40
|
+
* the wall clock and the L1 chain when computing L1 slot timestamps.
|
|
41
|
+
*/
|
|
42
|
+
dateProvider?: TestDateProvider;
|
|
29
43
|
} = {},
|
|
30
44
|
): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise<void> }> {
|
|
31
45
|
const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');
|
|
@@ -55,6 +69,7 @@ export async function startAnvil(
|
|
|
55
69
|
if (opts.hardfork !== undefined) {
|
|
56
70
|
args.push('--hardfork', opts.hardfork);
|
|
57
71
|
}
|
|
72
|
+
args.push('--slots-in-an-epoch', String(opts.slotsInAnEpoch ?? 1));
|
|
58
73
|
|
|
59
74
|
const child = spawn(anvilBinary, args, {
|
|
60
75
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -100,12 +115,15 @@ export async function startAnvil(
|
|
|
100
115
|
child.once('close', onClose);
|
|
101
116
|
});
|
|
102
117
|
|
|
103
|
-
// Continue piping for logging
|
|
104
|
-
if (logger || opts.captureMethodCalls) {
|
|
118
|
+
// Continue piping for logging, method-call capture, and/or dateProvider sync after startup.
|
|
119
|
+
if (logger || opts.captureMethodCalls || opts.dateProvider) {
|
|
105
120
|
child.stdout?.on('data', (data: Buffer) => {
|
|
106
121
|
const text = data.toString();
|
|
107
122
|
logger?.debug(text.trim());
|
|
108
123
|
methodCalls?.push(...(text.match(/eth_[^\s]+/g) || []));
|
|
124
|
+
if (opts.dateProvider) {
|
|
125
|
+
syncDateProviderFromAnvilOutput(text, opts.dateProvider);
|
|
126
|
+
}
|
|
109
127
|
});
|
|
110
128
|
child.stderr?.on('data', (data: Buffer) => {
|
|
111
129
|
logger?.debug(data.toString().trim());
|
|
@@ -152,6 +170,19 @@ export async function startAnvil(
|
|
|
152
170
|
return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` };
|
|
153
171
|
}
|
|
154
172
|
|
|
173
|
+
/** Extracts block time from anvil stdout and syncs the dateProvider. */
|
|
174
|
+
function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDateProvider): void {
|
|
175
|
+
// Anvil logs mined blocks as:
|
|
176
|
+
// Block Time: "Fri, 20 Mar 2026 02:10:46 +0000"
|
|
177
|
+
const match = text.match(/Block Time:\s*"([^"]+)"/);
|
|
178
|
+
if (match) {
|
|
179
|
+
const blockTimeMs = new Date(match[1]).getTime();
|
|
180
|
+
if (!isNaN(blockTimeMs)) {
|
|
181
|
+
dateProvider.setTime(blockTimeMs);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
155
186
|
/** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */
|
|
156
187
|
function killChild(child: ChildProcess): Promise<void> {
|
|
157
188
|
return new Promise<void>(resolve => {
|