@aztec-labs/ethereum 6.0.0-rc.3 → 6.0.0
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/blob_tx.d.ts +24 -0
- package/dest/blob_tx.d.ts.map +1 -0
- package/dest/blob_tx.js +94 -0
- package/dest/bn254/hash_to_point.d.ts +32 -0
- package/dest/bn254/hash_to_point.d.ts.map +1 -0
- package/dest/bn254/hash_to_point.js +80 -0
- package/dest/bn254/index.d.ts +3 -0
- package/dest/bn254/index.d.ts.map +1 -0
- package/dest/bn254/index.js +2 -0
- package/dest/bn254/proof_of_possession_gas.d.ts +29 -0
- package/dest/bn254/proof_of_possession_gas.d.ts.map +1 -0
- package/dest/bn254/proof_of_possession_gas.js +24 -0
- package/dest/client.d.ts +1 -1
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +5 -3
- package/dest/contracts/errors.d.ts +10 -3
- package/dest/contracts/errors.d.ts.map +1 -1
- package/dest/contracts/errors.js +10 -4
- package/dest/contracts/inbox.d.ts +1 -1
- package/dest/contracts/inbox.d.ts.map +1 -1
- package/dest/contracts/inbox.js +2 -0
- package/dest/contracts/log.d.ts +7 -1
- package/dest/contracts/log.d.ts.map +1 -1
- package/dest/contracts/log.js +6 -0
- package/dest/contracts/rollup.d.ts +32 -4
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +63 -7
- package/dest/deploy_aztec_l1_contracts.d.ts +16 -1
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +201 -65
- package/dest/foundry_binary.d.ts +1 -1
- package/dest/foundry_binary.d.ts.map +1 -1
- package/dest/foundry_binary.js +2 -1
- package/dest/l1_tx_utils/amsterdam.d.ts +1 -1
- package/dest/l1_tx_utils/amsterdam.d.ts.map +1 -1
- package/dest/l1_tx_utils/amsterdam.js +4 -1
- package/dest/l1_tx_utils/constants.d.ts +2 -1
- package/dest/l1_tx_utils/constants.d.ts.map +1 -1
- package/dest/l1_tx_utils/constants.js +3 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts +1 -1
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts.map +1 -1
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +6 -1
- package/dest/l1_tx_utils/index.d.ts +2 -1
- package/dest/l1_tx_utils/index.d.ts.map +1 -1
- package/dest/l1_tx_utils/index.js +1 -0
- package/dest/l1_tx_utils/l1_tx_utils.d.ts +21 -1
- package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_tx_utils.js +71 -5
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +8 -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 +1 -1
- package/dest/l1_tx_utils/send_cost.d.ts +15 -0
- package/dest/l1_tx_utils/send_cost.d.ts.map +1 -0
- package/dest/l1_tx_utils/send_cost.js +10 -0
- package/dest/l1_tx_utils/signer.d.ts +2 -2
- package/dest/l1_tx_utils/signer.d.ts.map +1 -1
- package/dest/l1_tx_utils/signer.js +2 -2
- package/dest/l1_tx_utils/tx_delayer.d.ts +1 -1
- package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
- package/dest/l1_tx_utils/tx_delayer.js +8 -26
- package/dest/publisher_manager.d.ts +127 -13
- package/dest/publisher_manager.d.ts.map +1 -1
- package/dest/publisher_manager.js +320 -79
- package/dest/test/anvil_hardfork.d.ts +42 -0
- package/dest/test/anvil_hardfork.d.ts.map +1 -0
- package/dest/test/anvil_hardfork.js +107 -0
- package/dest/test/eth_cheat_codes.d.ts +30 -5
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +283 -101
- package/dest/test/index.d.ts +3 -1
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +2 -0
- package/dest/test/l1_hardfork.d.ts +43 -0
- package/dest/test/l1_hardfork.d.ts.map +1 -0
- package/dest/test/l1_hardfork.js +51 -0
- package/dest/test/recording_rpc_server.d.ts +1 -1
- package/dest/test/recording_rpc_server.d.ts.map +1 -1
- package/dest/test/recording_rpc_server.js +1 -0
- package/dest/test/rollup_cheat_codes.d.ts +1 -1
- package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
- package/dest/test/rollup_cheat_codes.js +7 -7
- package/dest/test/start_anvil.d.ts +14 -2
- package/dest/test/start_anvil.d.ts.map +1 -1
- package/dest/test/start_anvil.js +28 -11
- package/package.json +13 -11
- package/src/blob_tx.ts +118 -0
- package/src/bn254/hash_to_point.ts +95 -0
- package/src/bn254/index.ts +2 -0
- package/src/bn254/proof_of_possession_gas.ts +54 -0
- package/src/client.ts +6 -3
- package/src/contracts/errors.ts +12 -4
- package/src/contracts/inbox.ts +4 -0
- package/src/contracts/log.ts +12 -0
- package/src/contracts/rollup.ts +63 -6
- package/src/deploy_aztec_l1_contracts.ts +238 -63
- package/src/foundry_binary.ts +2 -1
- package/src/l1_tx_utils/amsterdam.ts +4 -1
- package/src/l1_tx_utils/constants.ts +4 -0
- package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +11 -1
- package/src/l1_tx_utils/index.ts +1 -0
- package/src/l1_tx_utils/l1_tx_utils.ts +72 -5
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +1 -1
- package/src/l1_tx_utils/send_cost.ts +21 -0
- package/src/l1_tx_utils/signer.ts +3 -2
- package/src/l1_tx_utils/tx_delayer.ts +8 -37
- package/src/publisher_manager.ts +458 -83
- package/src/test/anvil_hardfork.ts +95 -0
- package/src/test/eth_cheat_codes.ts +306 -81
- package/src/test/fixtures/sepolia_blob_tx.json +10 -0
- package/src/test/index.ts +2 -0
- package/src/test/l1_hardfork.ts +71 -0
- package/src/test/recording_rpc_server.ts +1 -0
- package/src/test/rollup_cheat_codes.ts +9 -9
- package/src/test/start_anvil.ts +41 -13
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { pick } from '@aztec-labs/foundation/collection';
|
|
1
|
+
import { maxBy, pick } from '@aztec-labs/foundation/collection';
|
|
2
2
|
import { createLogger } from '@aztec-labs/foundation/log';
|
|
3
3
|
import { RunningPromise } from '@aztec-labs/foundation/running-promise';
|
|
4
|
+
import { formatEther } from 'viem';
|
|
4
5
|
import { Multicall3 } from './contracts/multicall.js';
|
|
5
|
-
import { TxUtilsState } from './l1_tx_utils/index.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
TxUtilsState.
|
|
12
|
-
|
|
13
|
-
TxUtilsState.SPEED_UP,
|
|
14
|
-
TxUtilsState.SENT,
|
|
15
|
-
|
|
16
|
-
TxUtilsState.
|
|
17
|
-
|
|
18
|
-
];
|
|
6
|
+
import { INSUFFICIENT_FUNDS_BACKOFF_MS, TxUtilsState, computeSendCost } from './l1_tx_utils/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Selection rank per publisher state (lower is better). IDLE and MINED rank equally: a publisher whose sends keep
|
|
9
|
+
* failing (e.g. for lack of funds) never leaves IDLE, so ranking IDLE above MINED would let it beat a working one.
|
|
10
|
+
* In-flight states come next, and cancelled or not-mined states last since they represent failures to mine.
|
|
11
|
+
*/ const stateRank = {
|
|
12
|
+
[TxUtilsState.IDLE]: 0,
|
|
13
|
+
[TxUtilsState.MINED]: 0,
|
|
14
|
+
[TxUtilsState.SPEED_UP]: 1,
|
|
15
|
+
[TxUtilsState.SENT]: 1,
|
|
16
|
+
[TxUtilsState.CANCELLED]: 2,
|
|
17
|
+
[TxUtilsState.NOT_MINED]: 2
|
|
18
|
+
};
|
|
19
19
|
// Which states represent a busy publisher that we should avoid if possible
|
|
20
20
|
const busyStates = [
|
|
21
21
|
TxUtilsState.SENT,
|
|
@@ -23,22 +23,75 @@ const busyStates = [
|
|
|
23
23
|
TxUtilsState.CANCELLED,
|
|
24
24
|
TxUtilsState.NOT_MINED
|
|
25
25
|
];
|
|
26
|
+
const LOW_BALANCE_WARNING_INTERVAL_MS = 10 * 60 * 1000;
|
|
27
|
+
const DEFAULT_LOW_BALANCE_WARNING_MULTIPLIER = 4;
|
|
28
|
+
const NO_AFFORDABLE_PUBLISHER_WARNING_KEY = 'no-affordable-publisher';
|
|
29
|
+
/** Thrown when publishers are available but none holds enough ETH to afford the requested L1 send. */ export class NoAffordablePublisherError extends Error {
|
|
30
|
+
required;
|
|
31
|
+
requirement;
|
|
32
|
+
fees;
|
|
33
|
+
publishers;
|
|
34
|
+
constructor(required, requirement, fees, publishers){
|
|
35
|
+
super(`No publisher can afford the L1 send: requires ${formatEther(required)} ETH ` + `(balances: ${publishers.map((p)=>`${p.address}=${formatEther(p.balance)}`).join(', ') || 'none'})`), this.required = required, this.requirement = requirement, this.fees = fees, this.publishers = publishers;
|
|
36
|
+
this.name = 'NoAffordablePublisherError';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* ETH the best funded publisher that is not backed off after an insufficient funds rejection is missing to afford the
|
|
40
|
+
* send. Undefined if every publisher is backed off.
|
|
41
|
+
*/ shortfall() {
|
|
42
|
+
const richest = this.getRichestNotBackedOff();
|
|
43
|
+
return richest === undefined ? undefined : this.required - richest.balance;
|
|
44
|
+
}
|
|
45
|
+
/** Tells the operator which publisher to fund and with how much ETH for the send to become affordable. */ describeFunding() {
|
|
46
|
+
const richest = this.getRichestNotBackedOff();
|
|
47
|
+
if (richest === undefined || this.required <= richest.balance) {
|
|
48
|
+
const minutes = INSUFFICIENT_FUNDS_BACKOFF_MS / 60_000;
|
|
49
|
+
return `every publisher is backed off after an insufficient funds rejection ` + `until its balance increases or ${minutes} minutes pass`;
|
|
50
|
+
}
|
|
51
|
+
const shortfall = formatEther(this.required - richest.balance);
|
|
52
|
+
const others = this.publishers.filter((p)=>p.address !== richest.address);
|
|
53
|
+
const alternative = others.length === 0 ? '' : `, or top up any of ${others.map((p)=>p.address).join(', ')} to ${formatEther(this.required)} ETH`;
|
|
54
|
+
return `fund ${richest.address} with ${shortfall} ETH more${alternative}`;
|
|
55
|
+
}
|
|
56
|
+
/** Structured context for logging. */ toLogContext() {
|
|
57
|
+
return {
|
|
58
|
+
required: this.required,
|
|
59
|
+
shortfall: this.shortfall(),
|
|
60
|
+
gasLimit: this.requirement.gasLimit,
|
|
61
|
+
blobCount: this.requirement.blobCount,
|
|
62
|
+
maxFeePerGas: this.fees.maxFeePerGas,
|
|
63
|
+
maxFeePerBlobGas: this.fees.maxFeePerBlobGas,
|
|
64
|
+
publishers: this.publishers
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
getRichestNotBackedOff() {
|
|
68
|
+
return maxBy(this.publishers.filter((p)=>p.insufficientFundsAtBalance === undefined), (p)=>p.balance);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
26
71
|
export class PublisherManager {
|
|
27
72
|
publishers;
|
|
73
|
+
static BALANCE_CHECK_INTERVAL_MS = 60 * 1000;
|
|
28
74
|
log;
|
|
29
75
|
config;
|
|
30
|
-
static FUNDING_CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
|
31
76
|
funder;
|
|
32
|
-
|
|
77
|
+
balanceCheckPromise;
|
|
33
78
|
started;
|
|
79
|
+
metrics;
|
|
80
|
+
referenceSend;
|
|
81
|
+
/** Whether the balance check has run since the last start, so the first one logs a summary of all balances. */ loggedBalanceSummary;
|
|
82
|
+
/** Last time a balance warning was logged, per publisher address or per kind of warning. */ lastLowBalanceWarningAt;
|
|
34
83
|
constructor(publishers, config, opts){
|
|
35
84
|
this.publishers = publishers;
|
|
36
85
|
this.started = false;
|
|
86
|
+
this.loggedBalanceSummary = false;
|
|
87
|
+
this.lastLowBalanceWarningAt = new Map();
|
|
37
88
|
this.funder = opts?.funder;
|
|
89
|
+
this.metrics = opts?.metrics;
|
|
90
|
+
this.referenceSend = opts?.referenceSend;
|
|
38
91
|
this.log = createLogger('publisher:manager', opts?.bindings);
|
|
39
92
|
this.log.info(`PublisherManager initialized with ${publishers.length} publishers.`);
|
|
40
93
|
this.publishers = publishers;
|
|
41
|
-
this.config = pick(config, 'publisherAllowInvalidStates', 'publisherFundingThreshold', 'publisherFundingAmount');
|
|
94
|
+
this.config = pick(config, 'publisherAllowInvalidStates', 'publisherFundingThreshold', 'publisherFundingAmount', 'publisherLowBalanceWarningMultiplier');
|
|
42
95
|
const hasThreshold = this.config.publisherFundingThreshold !== undefined;
|
|
43
96
|
const hasAmount = this.config.publisherFundingAmount !== undefined;
|
|
44
97
|
if (hasThreshold !== hasAmount) {
|
|
@@ -51,16 +104,14 @@ export class PublisherManager {
|
|
|
51
104
|
this.funder = undefined;
|
|
52
105
|
}
|
|
53
106
|
}
|
|
54
|
-
|
|
55
|
-
this.fundingPromise = new RunningPromise(()=>this.triggerFundingIfNeeded(), this.log, PublisherManager.FUNDING_CHECK_INTERVAL_MS);
|
|
56
|
-
}
|
|
107
|
+
this.balanceCheckPromise = new RunningPromise(()=>this.checkBalances(), this.log, PublisherManager.BALANCE_CHECK_INTERVAL_MS);
|
|
57
108
|
}
|
|
58
109
|
/**
|
|
59
110
|
* Clears any interrupted flag left by a previous {@link stop} so publishing works again after a restart,
|
|
60
|
-
* loads the state of all publishers and the funder, and starts periodic
|
|
61
|
-
* start while already started is a no-op, so it never re-runs
|
|
62
|
-
* would spawn a duplicate background monitor per pending nonce). Lifecycle
|
|
63
|
-
* serialized by the caller.
|
|
111
|
+
* loads the state of all publishers and the funder, and starts the periodic balance check, which also tops up
|
|
112
|
+
* publishers if funding is configured. Idempotent: a start while already started is a no-op, so it never re-runs
|
|
113
|
+
* `loadStateAndResumeMonitoring` (which would spawn a duplicate background monitor per pending nonce). Lifecycle
|
|
114
|
+
* calls are expected to be serialized by the caller.
|
|
64
115
|
*/ async start() {
|
|
65
116
|
if (this.started) {
|
|
66
117
|
this.log.debug('PublisherManager already started, ignoring start');
|
|
@@ -74,17 +125,20 @@ export class PublisherManager {
|
|
|
74
125
|
...this.publishers.map((pub)=>pub.loadStateAndResumeMonitoring()),
|
|
75
126
|
this.funder?.loadStateAndResumeMonitoring()
|
|
76
127
|
]);
|
|
77
|
-
this.
|
|
128
|
+
this.loggedBalanceSummary = false;
|
|
129
|
+
this.lastLowBalanceWarningAt.delete(NO_AFFORDABLE_PUBLISHER_WARNING_KEY);
|
|
130
|
+
this.balanceCheckPromise.start();
|
|
78
131
|
// Marked started only once fully up, so a start that failed to load state can be retried.
|
|
79
132
|
this.started = true;
|
|
80
133
|
}
|
|
81
134
|
/**
|
|
82
|
-
* Stops the
|
|
135
|
+
* Stops the balance check loop, interrupts all publishers so no further L1 txs are sent, and waits (bounded)
|
|
83
136
|
* for their in-flight tx monitor loops to wind down. Idempotent, and the manager may be restarted
|
|
84
137
|
* afterwards via {@link start}, which clears the interrupted flag.
|
|
85
138
|
*/ async stop() {
|
|
86
139
|
this.started = false;
|
|
87
|
-
await this.
|
|
140
|
+
await this.balanceCheckPromise.stop();
|
|
141
|
+
this.metrics?.recordBalanceSnapshot(undefined);
|
|
88
142
|
this.publishers.forEach((pub)=>pub.interrupt());
|
|
89
143
|
this.funder?.interrupt();
|
|
90
144
|
// Wait for in-flight tx monitor loops to observe the interrupt, so no L1 requests are still
|
|
@@ -94,76 +148,264 @@ export class PublisherManager {
|
|
|
94
148
|
this.funder?.waitMonitoringStopped()
|
|
95
149
|
]);
|
|
96
150
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Finds and prioritises an available publisher. Candidates must pass the filter and not be busy (unless
|
|
153
|
+
* `publisherAllowInvalidStates` is set and no idle publisher remains), and are ranked by state (see `stateRank`), then
|
|
154
|
+
* highest balance, then least recently used. Publishers backed off after an insufficient funds rejection are skipped
|
|
155
|
+
* until their balance increases or the backoff expires.
|
|
156
|
+
*
|
|
157
|
+
* With a `requirement`, only publishers whose balance covers the worst-case cost of that send are eligible, and a
|
|
158
|
+
* {@link NoAffordablePublisherError} is thrown if there are none. Without one, selection is lenient: it prefers funded
|
|
159
|
+
* publishers but falls back to unfunded ones rather than failing, for sends that are fine to attempt and lose.
|
|
160
|
+
*/ async getAvailablePublisher(filter = ()=>true, opts) {
|
|
104
161
|
this.log.debug(`Getting available publisher`, {
|
|
105
162
|
publishers: this.publishers.map((p)=>({
|
|
106
163
|
address: p.getSenderAddress(),
|
|
107
164
|
state: p.state,
|
|
108
165
|
lastMined: p.lastMinedAtBlockNumber
|
|
109
|
-
}))
|
|
166
|
+
})),
|
|
167
|
+
requirement: opts?.requirement
|
|
110
168
|
});
|
|
111
|
-
|
|
112
|
-
let validPublishers =
|
|
113
|
-
// If none found but we allow invalid (busy) states, try again including them
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
169
|
+
const filtered = this.publishers.filter((pub)=>filter(pub));
|
|
170
|
+
let validPublishers = filtered.filter((pub)=>!busyStates.includes(pub.state));
|
|
171
|
+
// If none found but we allow invalid (busy) states, try again including them. When the send has a requirement we
|
|
172
|
+
// also consider busy ones, so that an idle but unaffordable publisher does not hide a busy affordable one. Busy
|
|
173
|
+
// publishers still rank below idle ones.
|
|
174
|
+
if (this.config.publisherAllowInvalidStates && (validPublishers.length === 0 || opts?.requirement)) {
|
|
175
|
+
if (validPublishers.length === 0) {
|
|
176
|
+
this.log.warn(`No valid publishers found. Trying again including invalid states.`);
|
|
177
|
+
}
|
|
178
|
+
validPublishers = filtered;
|
|
179
|
+
}
|
|
119
180
|
if (validPublishers.length === 0) {
|
|
120
181
|
throw new Error(`Failed to find an available publisher.`);
|
|
121
182
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// set only if every candidate is unfunded, so behaviour is no worse than before.
|
|
132
|
-
let fundedPublishers = publishersWithBalance.filter((p)=>p.balance > 0n);
|
|
133
|
-
if (fundedPublishers.length === 0) {
|
|
134
|
-
this.log.warn(`All candidate publishers have zero balance; selecting from unfunded publishers.`);
|
|
135
|
-
fundedPublishers = publishersWithBalance;
|
|
136
|
-
}
|
|
137
|
-
// Sort based on state, then balance, then time since last use
|
|
138
|
-
const sortedPublishers = fundedPublishers.sort((a, b)=>{
|
|
139
|
-
const stateComparison = sortOrder.indexOf(a.publisher.state) - sortOrder.indexOf(b.publisher.state);
|
|
183
|
+
const withBalance = await this.getBalances(validPublishers);
|
|
184
|
+
let candidates;
|
|
185
|
+
if (opts?.requirement) {
|
|
186
|
+
candidates = await this.getAffordable(withBalance, opts.requirement);
|
|
187
|
+
} else {
|
|
188
|
+
candidates = this.getLenientCandidates(withBalance);
|
|
189
|
+
}
|
|
190
|
+
const sortedPublishers = candidates.sort((a, b)=>{
|
|
191
|
+
const stateComparison = stateRank[a.publisher.state] - stateRank[b.publisher.state];
|
|
140
192
|
if (stateComparison !== 0) {
|
|
141
193
|
return stateComparison;
|
|
142
194
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return balanceComparison;
|
|
195
|
+
if (a.balance !== b.balance) {
|
|
196
|
+
return b.balance > a.balance ? 1 : -1;
|
|
146
197
|
}
|
|
147
198
|
const lastUsedComparison = Number((a.publisher.lastMinedAtBlockNumber ?? 0n) - (b.publisher.lastMinedAtBlockNumber ?? 0n));
|
|
148
199
|
return lastUsedComparison;
|
|
149
200
|
});
|
|
150
201
|
return sortedPublishers[0].publisher;
|
|
151
202
|
}
|
|
152
|
-
/**
|
|
203
|
+
/**
|
|
204
|
+
* Reads the balance of every publisher and the funder, prices the reference send at current fees, records metrics,
|
|
205
|
+
* logs balances that are low or cannot afford the reference send, and tops up publishers below the funding threshold
|
|
206
|
+
* if funding is configured. Runs once a minute while started.
|
|
207
|
+
*/ async checkBalances() {
|
|
208
|
+
const funder = this.funder;
|
|
209
|
+
const [publisherBalances, funderBalance] = await Promise.all([
|
|
210
|
+
Promise.all(this.publishers.map((publisher)=>this.tryGetBalance(publisher))),
|
|
211
|
+
funder ? this.tryGetBalance(funder) : undefined
|
|
212
|
+
]);
|
|
213
|
+
const withBalance = this.publishers.flatMap((publisher, i)=>{
|
|
214
|
+
const balance = publisherBalances[i];
|
|
215
|
+
return balance === undefined ? [] : [
|
|
216
|
+
{
|
|
217
|
+
publisher,
|
|
218
|
+
balance
|
|
219
|
+
}
|
|
220
|
+
];
|
|
221
|
+
});
|
|
222
|
+
const cost = await this.tryGetReferenceCost();
|
|
223
|
+
const required = cost !== undefined && cost.required > 0n ? cost.required : undefined;
|
|
224
|
+
const snapshot = {
|
|
225
|
+
publishers: withBalance.map(({ publisher, balance })=>({
|
|
226
|
+
address: publisher.getSenderAddress().toString(),
|
|
227
|
+
balance,
|
|
228
|
+
sendsAffordable: required === undefined ? undefined : publisher.hasActiveInsufficientFundsBackoff(balance) ? 0 : Number(balance / required)
|
|
229
|
+
})),
|
|
230
|
+
funder: funder && funderBalance !== undefined ? {
|
|
231
|
+
address: funder.getSenderAddress().toString(),
|
|
232
|
+
balance: funderBalance
|
|
233
|
+
} : undefined,
|
|
234
|
+
required
|
|
235
|
+
};
|
|
236
|
+
this.metrics?.recordBalanceSnapshot(snapshot);
|
|
237
|
+
this.logBalances(withBalance, snapshot, cost?.fees);
|
|
238
|
+
if (required !== undefined) {
|
|
239
|
+
this.warnOnLowBalances(withBalance, required);
|
|
240
|
+
}
|
|
241
|
+
await this.fundIfNeeded(withBalance, funderBalance);
|
|
242
|
+
}
|
|
243
|
+
getBalances(publishers) {
|
|
244
|
+
return Promise.all(publishers.map(async (publisher)=>({
|
|
245
|
+
publisher,
|
|
246
|
+
balance: await publisher.getSenderBalance()
|
|
247
|
+
})));
|
|
248
|
+
}
|
|
249
|
+
/** Returns the publishers that can afford the requirement, or throws NoAffordablePublisherError if none. */ async getAffordable(publishers, requirement) {
|
|
250
|
+
if (publishers.length === 0) {
|
|
251
|
+
throw new Error(`Failed to find an available publisher.`);
|
|
252
|
+
}
|
|
253
|
+
// All publishers share the L1 network, so fees are fetched once rather than per publisher.
|
|
254
|
+
const fees = await publishers[0].publisher.getFeesPerGas(undefined, requirement.blobCount > 0, 0);
|
|
255
|
+
const required = computeSendCost(fees, requirement);
|
|
256
|
+
const affordable = publishers.filter((p)=>p.balance >= required && !p.publisher.isBackedOffForInsufficientFunds(p.balance));
|
|
257
|
+
if (affordable.length === 0) {
|
|
258
|
+
throw new NoAffordablePublisherError(required, requirement, fees, this.toBalanceInfo(publishers));
|
|
259
|
+
}
|
|
260
|
+
return affordable;
|
|
261
|
+
}
|
|
262
|
+
toBalanceInfo(publishers) {
|
|
263
|
+
return publishers.map(({ publisher, balance })=>({
|
|
264
|
+
address: publisher.getSenderAddress().toString(),
|
|
265
|
+
balance,
|
|
266
|
+
state: TxUtilsState[publisher.state],
|
|
267
|
+
insufficientFundsAtBalance: publisher.hasActiveInsufficientFundsBackoff(balance) ? publisher.getInsufficientFundsAtBalance() : undefined
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Prefers publishers that are funded and not backed off after an insufficient funds rejection, but never returns an
|
|
272
|
+
* empty list: if every candidate is excluded, falls back to the full set so lenient sends still have something to try.
|
|
273
|
+
*/ getLenientCandidates(publishers) {
|
|
274
|
+
const usable = publishers.filter((p)=>p.balance > 0n && !p.publisher.isBackedOffForInsufficientFunds(p.balance));
|
|
275
|
+
if (usable.length > 0) {
|
|
276
|
+
return usable;
|
|
277
|
+
}
|
|
278
|
+
const funded = publishers.filter((p)=>p.balance > 0n);
|
|
279
|
+
if (funded.length > 0) {
|
|
280
|
+
return funded;
|
|
281
|
+
}
|
|
282
|
+
this.log.warn(`All candidate publishers have zero balance; selecting from unfunded publishers.`);
|
|
283
|
+
return publishers;
|
|
284
|
+
}
|
|
285
|
+
async tryGetBalance(utils) {
|
|
286
|
+
try {
|
|
287
|
+
return await utils.getSenderBalance();
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const address = utils.getSenderAddress().toString();
|
|
290
|
+
if (this.shouldWarn(`balance-read:${address}`)) {
|
|
291
|
+
this.log.warn(`Failed to read L1 balance of account`, {
|
|
292
|
+
address,
|
|
293
|
+
err
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async tryGetReferenceCost() {
|
|
300
|
+
const { referenceSend } = this;
|
|
301
|
+
if (referenceSend === undefined || this.publishers.length === 0) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
// All publishers share the L1 network, so fees are fetched once rather than per publisher.
|
|
306
|
+
const fees = await this.publishers[0].getFeesPerGas(undefined, referenceSend.requirement.blobCount > 0, 0);
|
|
307
|
+
return {
|
|
308
|
+
fees,
|
|
309
|
+
required: computeSendCost(fees, referenceSend.requirement)
|
|
310
|
+
};
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (this.shouldWarn('fee-read')) {
|
|
313
|
+
this.log.warn(`Failed to read L1 fees to check whether publishers can afford a ${referenceSend.name}`, {
|
|
314
|
+
err
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Logs a summary of all balances on the first check after a start, and an error (at most once per interval) while no
|
|
322
|
+
* publisher can afford the reference send.
|
|
323
|
+
*/ logBalances(publishers, snapshot, fees) {
|
|
324
|
+
const firstCheck = !this.loggedBalanceSummary;
|
|
325
|
+
this.loggedBalanceSummary = true;
|
|
326
|
+
const { referenceSend } = this;
|
|
327
|
+
const { required } = snapshot;
|
|
328
|
+
const context = {
|
|
329
|
+
publishers: snapshot.publishers.map((p)=>({
|
|
330
|
+
address: p.address,
|
|
331
|
+
balance: formatEther(p.balance),
|
|
332
|
+
sendsAffordable: p.sendsAffordable
|
|
333
|
+
})),
|
|
334
|
+
referenceSend: referenceSend?.name,
|
|
335
|
+
required: required === undefined ? undefined : formatEther(required),
|
|
336
|
+
funder: snapshot.funder && {
|
|
337
|
+
address: snapshot.funder.address,
|
|
338
|
+
balance: formatEther(snapshot.funder.balance)
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const allRead = publishers.length === this.publishers.length;
|
|
342
|
+
const noneAffordable = referenceSend !== undefined && required !== undefined && fees !== undefined && allRead && snapshot.publishers.length > 0 && snapshot.publishers.every((p)=>(p.sendsAffordable ?? 0) < 1);
|
|
343
|
+
if (!noneAffordable) {
|
|
344
|
+
// Only a publisher seen affording the send ends an episode. A check that could not tell (failed reads) must not
|
|
345
|
+
// reset the rate limit, or alternating failed and empty checks would log the error on every empty one.
|
|
346
|
+
if (snapshot.publishers.some((p)=>(p.sendsAffordable ?? 0) >= 1)) {
|
|
347
|
+
this.lastLowBalanceWarningAt.delete(NO_AFFORDABLE_PUBLISHER_WARNING_KEY);
|
|
348
|
+
}
|
|
349
|
+
if (firstCheck) {
|
|
350
|
+
this.log.info(`Publisher balances`, context);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (this.shouldWarn(NO_AFFORDABLE_PUBLISHER_WARNING_KEY)) {
|
|
355
|
+
const err = new NoAffordablePublisherError(required, referenceSend.requirement, fees, this.toBalanceInfo(publishers));
|
|
356
|
+
this.log.error(`No publisher can afford a ${referenceSend.name} at current L1 fees: ${err.describeFunding()}`, {
|
|
357
|
+
...context,
|
|
358
|
+
shortfall: err.shortfall()
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** Warns at most once per interval for every publisher below the low-balance level. */ warnOnLowBalances(publishers, required) {
|
|
363
|
+
const multiplier = this.config.publisherLowBalanceWarningMultiplier ?? DEFAULT_LOW_BALANCE_WARNING_MULTIPLIER;
|
|
364
|
+
const warningLevel = required * BigInt(Math.round(multiplier * 1000)) / 1000n;
|
|
365
|
+
for (const { publisher, balance } of publishers){
|
|
366
|
+
if (balance >= warningLevel) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const address = publisher.getSenderAddress().toString();
|
|
370
|
+
if (this.shouldWarn(address)) {
|
|
371
|
+
this.log.warn(`Publisher balance is low relative to the ETH required for an L1 send`, {
|
|
372
|
+
address,
|
|
373
|
+
balance,
|
|
374
|
+
required,
|
|
375
|
+
multiplier,
|
|
376
|
+
warningLevel
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const { publisherFundingThreshold } = this.config;
|
|
381
|
+
if (this.funder && publisherFundingThreshold !== undefined && publisherFundingThreshold < warningLevel && this.shouldWarn('funding-threshold')) {
|
|
382
|
+
this.log.warn(`Publisher funding threshold is below the low-balance warning level`, {
|
|
383
|
+
publisherFundingThreshold,
|
|
384
|
+
warningLevel,
|
|
385
|
+
required,
|
|
386
|
+
multiplier
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
shouldWarn(key) {
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
const last = this.lastLowBalanceWarningAt.get(key);
|
|
393
|
+
if (last !== undefined && now - last < LOW_BALANCE_WARNING_INTERVAL_MS) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
this.lastLowBalanceWarningAt.set(key, now);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
/** Funds the publishers below the funding threshold, using the balances read in the same balance check. */ async fundIfNeeded(publishers, funderBalance) {
|
|
153
400
|
const { funder, config } = this;
|
|
154
|
-
|
|
401
|
+
const { publisherFundingThreshold, publisherFundingAmount: fundingAmount } = config;
|
|
402
|
+
if (!funder || publisherFundingThreshold === undefined || fundingAmount === undefined) {
|
|
155
403
|
return;
|
|
156
404
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
publisher: pub
|
|
160
|
-
})));
|
|
161
|
-
const lowBalance = allBalances.filter((p)=>p.balance < config.publisherFundingThreshold);
|
|
162
|
-
if (lowBalance.length === 0) {
|
|
405
|
+
const lowBalance = publishers.filter((p)=>p.balance < publisherFundingThreshold);
|
|
406
|
+
if (lowBalance.length === 0 || funderBalance === undefined) {
|
|
163
407
|
return;
|
|
164
408
|
}
|
|
165
|
-
const fundingAmount = config.publisherFundingAmount;
|
|
166
|
-
const funderBalance = await funder.getSenderBalance();
|
|
167
409
|
if (funderBalance < 10n * fundingAmount) {
|
|
168
410
|
this.log.warn(`Funding account balance is low`, {
|
|
169
411
|
funderBalance,
|
|
@@ -185,15 +427,14 @@ export class PublisherManager {
|
|
|
185
427
|
});
|
|
186
428
|
}
|
|
187
429
|
const toFund = lowBalance.slice(0, affordableCount).map((p)=>p.publisher);
|
|
188
|
-
await this.fundPublishers(toFund);
|
|
430
|
+
await this.fundPublishers(funder, toFund, fundingAmount);
|
|
189
431
|
}
|
|
190
|
-
/** Fund publishers via a single Multicall3 aggregate3Value transaction. */ async fundPublishers(publishers) {
|
|
191
|
-
const fundingAmount = this.config.publisherFundingAmount;
|
|
432
|
+
/** Fund publishers via a single Multicall3 aggregate3Value transaction. */ async fundPublishers(funder, publishers, fundingAmount) {
|
|
192
433
|
const calls = publishers.map((pub)=>({
|
|
193
434
|
to: pub.getSenderAddress().toString(),
|
|
194
435
|
value: fundingAmount
|
|
195
436
|
}));
|
|
196
|
-
await Multicall3.forwardValue(calls,
|
|
437
|
+
await Multicall3.forwardValue(calls, funder, this.log);
|
|
197
438
|
this.log.info(`Funded ${publishers.length} publishers`);
|
|
198
439
|
}
|
|
199
440
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type EIP1193RequestFn } from 'viem';
|
|
2
|
+
import type { EthCheatCodes } from './eth_cheat_codes.js';
|
|
3
|
+
import { type Anvil, startAnvil } from './start_anvil.js';
|
|
4
|
+
/**
|
|
5
|
+
* Starts a throwaway anvil on `hardfork` and checks that it mines blocks of that fork, so a test that needs a fork
|
|
6
|
+
* the installed foundry cannot run fails within seconds instead of after its whole setup. Never skips.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertAnvilSupportsHardfork(hardfork: string): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Writes anvil's state, including the historical states it still keeps, as the plain JSON file `--load-state` reads.
|
|
11
|
+
* Without history, reads at blocks before a restart fail on the reloaded anvil. A dump with history easily exceeds
|
|
12
|
+
* viem's default response size limit, so `client` must be one built without it, such as `createExtendedL1Client`'s.
|
|
13
|
+
*/
|
|
14
|
+
export declare function dumpAnvilStateWithHistory(client: {
|
|
15
|
+
request: EIP1193RequestFn;
|
|
16
|
+
}, path: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Moves an interval-mining anvil to another hardfork, continuing its chain. Anvil cannot change hardfork at runtime,
|
|
19
|
+
* so this stops mining, dumps the state with history to `stateDir` using `dumpClient`, stops `anvil`, and starts a new
|
|
20
|
+
* anvil on the same port from the dump with `startOpts`. Mining stays stopped until the old anvil is gone, so no block
|
|
21
|
+
* mined after the dump is lost. Pending txs are lost, since the mempool is not part of the dump.
|
|
22
|
+
*
|
|
23
|
+
* The reloaded anvil would stamp its next block as if no time had passed since the dump, so the first block on the new
|
|
24
|
+
* hardfork is mined at the first L1 slot of the old chain's grid that is not in the past, and interval mining resumes
|
|
25
|
+
* from there.
|
|
26
|
+
*
|
|
27
|
+
* @returns The new anvil, which replaces `anvil`, and the first block mined on `hardfork`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function restartAnvilOnHardfork(anvil: Anvil, cheat: EthCheatCodes, dumpClient: {
|
|
30
|
+
request: EIP1193RequestFn;
|
|
31
|
+
}, opts: {
|
|
32
|
+
hardfork: string;
|
|
33
|
+
stateDir: string;
|
|
34
|
+
startOpts?: Parameters<typeof startAnvil>[0];
|
|
35
|
+
}): Promise<{
|
|
36
|
+
anvil: Anvil;
|
|
37
|
+
forkBlock: {
|
|
38
|
+
number: bigint;
|
|
39
|
+
timestamp: bigint;
|
|
40
|
+
};
|
|
41
|
+
}>;
|
|
42
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYW52aWxfaGFyZGZvcmsuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90ZXN0L2FudmlsX2hhcmRmb3JrLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlBLE9BQU8sRUFBRSxLQUFLLGdCQUFnQixFQUFzQyxNQUFNLE1BQU0sQ0FBQztBQUlqRixPQUFPLEtBQUssRUFBRSxhQUFhLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUMxRCxPQUFPLEVBQUUsS0FBSyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFMUQ7OztHQUdHO0FBQ0gsd0JBQXNCLDJCQUEyQixDQUFDLFFBQVEsRUFBRSxNQUFNLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQWtCakY7QUFFRDs7OztHQUlHO0FBQ0gsd0JBQXNCLHlCQUF5QixDQUFDLE1BQU0sRUFBRTtJQUFFLE9BQU8sRUFBRSxnQkFBZ0IsQ0FBQTtDQUFFLEVBQUUsSUFBSSxFQUFFLE1BQU0sR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBU2xIO0FBRUQ7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCx3QkFBc0Isc0JBQXNCLENBQzFDLEtBQUssRUFBRSxLQUFLLEVBQ1osS0FBSyxFQUFFLGFBQWEsRUFDcEIsVUFBVSxFQUFFO0lBQUUsT0FBTyxFQUFFLGdCQUFnQixDQUFBO0NBQUUsRUFDekMsSUFBSSxFQUFFO0lBQUUsUUFBUSxFQUFFLE1BQU0sQ0FBQztJQUFDLFFBQVEsRUFBRSxNQUFNLENBQUM7SUFBQyxTQUFTLENBQUMsRUFBRSxVQUFVLENBQUMsT0FBTyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQTtDQUFFLEdBQ3pGLE9BQU8sQ0FBQztJQUFFLEtBQUssRUFBRSxLQUFLLENBQUM7SUFBQyxTQUFTLEVBQUU7UUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDO1FBQUMsU0FBUyxFQUFFLE1BQU0sQ0FBQTtLQUFFLENBQUE7Q0FBRSxDQUFDLENBMEI3RSJ9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anvil_hardfork.d.ts","sourceRoot":"","sources":["../../src/test/anvil_hardfork.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,gBAAgB,EAAsC,MAAM,MAAM,CAAC;AAIjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,KAAK,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBjF;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,gBAAgB,CAAA;CAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASlH;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,aAAa,EACpB,UAAU,EAAE;IAAE,OAAO,EAAE,gBAAgB,CAAA;CAAE,EACzC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;CAAE,GACzF,OAAO,CAAC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CA0B7E"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { gunzipSync } from 'node:zlib';
|
|
5
|
+
import { createPublicClient, http } from 'viem';
|
|
6
|
+
import { resolveFoundryBinary } from '../foundry_binary.js';
|
|
7
|
+
import { isAmsterdamBlock } from '../l1_tx_utils/amsterdam.js';
|
|
8
|
+
import { startAnvil } from './start_anvil.js';
|
|
9
|
+
/**
|
|
10
|
+
* Starts a throwaway anvil on `hardfork` and checks that it mines blocks of that fork, so a test that needs a fork
|
|
11
|
+
* the installed foundry cannot run fails within seconds instead of after its whole setup. Never skips.
|
|
12
|
+
*/ export async function assertAnvilSupportsHardfork(hardfork) {
|
|
13
|
+
let res;
|
|
14
|
+
try {
|
|
15
|
+
res = await startAnvil({
|
|
16
|
+
port: 0,
|
|
17
|
+
hardfork
|
|
18
|
+
});
|
|
19
|
+
const client = createPublicClient({
|
|
20
|
+
transport: http(res.rpcUrl, {
|
|
21
|
+
batch: false
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
await client.request({
|
|
25
|
+
method: 'evm_mine',
|
|
26
|
+
params: []
|
|
27
|
+
});
|
|
28
|
+
const block = await client.getBlock({
|
|
29
|
+
blockTag: 'latest'
|
|
30
|
+
});
|
|
31
|
+
if (isAmsterdamBlock(block) !== (hardfork === 'amsterdam')) {
|
|
32
|
+
throw new Error(`anvil --hardfork ${hardfork} mined block ${block.number} with the wrong header shape`);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
throw new Error(`Anvil at ${resolveFoundryBinary('anvil')} cannot run --hardfork ${hardfork}; foundry >= 1.8.5 is required`, {
|
|
36
|
+
cause: err
|
|
37
|
+
});
|
|
38
|
+
} finally{
|
|
39
|
+
await res?.stop();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Writes anvil's state, including the historical states it still keeps, as the plain JSON file `--load-state` reads.
|
|
44
|
+
* Without history, reads at blocks before a restart fail on the reloaded anvil. A dump with history easily exceeds
|
|
45
|
+
* viem's default response size limit, so `client` must be one built without it, such as `createExtendedL1Client`'s.
|
|
46
|
+
*/ export async function dumpAnvilStateWithHistory(client, path) {
|
|
47
|
+
const hex = await client.request({
|
|
48
|
+
method: 'anvil_dumpState',
|
|
49
|
+
params: [
|
|
50
|
+
true
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
const dump = Buffer.from(hex.slice(2), 'hex');
|
|
54
|
+
const isGzip = dump[0] === 0x1f && dump[1] === 0x8b;
|
|
55
|
+
// Kept as a Buffer: a dump with history can exceed the maximum length of a V8 string.
|
|
56
|
+
await writeFile(path, isGzip ? gunzipSync(dump) : dump);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Moves an interval-mining anvil to another hardfork, continuing its chain. Anvil cannot change hardfork at runtime,
|
|
60
|
+
* so this stops mining, dumps the state with history to `stateDir` using `dumpClient`, stops `anvil`, and starts a new
|
|
61
|
+
* anvil on the same port from the dump with `startOpts`. Mining stays stopped until the old anvil is gone, so no block
|
|
62
|
+
* mined after the dump is lost. Pending txs are lost, since the mempool is not part of the dump.
|
|
63
|
+
*
|
|
64
|
+
* The reloaded anvil would stamp its next block as if no time had passed since the dump, so the first block on the new
|
|
65
|
+
* hardfork is mined at the first L1 slot of the old chain's grid that is not in the past, and interval mining resumes
|
|
66
|
+
* from there.
|
|
67
|
+
*
|
|
68
|
+
* @returns The new anvil, which replaces `anvil`, and the first block mined on `hardfork`.
|
|
69
|
+
*/ export async function restartAnvilOnHardfork(anvil, cheat, dumpClient, opts) {
|
|
70
|
+
const blockTimeS = await cheat.getIntervalMining();
|
|
71
|
+
if (!blockTimeS) {
|
|
72
|
+
throw new Error('restartAnvilOnHardfork needs an anvil in interval mining');
|
|
73
|
+
}
|
|
74
|
+
await cheat.setIntervalMining(0, {
|
|
75
|
+
silent: true
|
|
76
|
+
});
|
|
77
|
+
const tip = await cheat.publicClient.getBlock({
|
|
78
|
+
blockTag: 'latest'
|
|
79
|
+
});
|
|
80
|
+
const statePath = join(opts.stateDir, `anvil-state-${tip.number}.json`);
|
|
81
|
+
await dumpAnvilStateWithHistory(dumpClient, statePath);
|
|
82
|
+
await anvil.stop();
|
|
83
|
+
const restarted = await startAnvil({
|
|
84
|
+
...opts.startOpts,
|
|
85
|
+
port: anvil.port,
|
|
86
|
+
hardfork: opts.hardfork,
|
|
87
|
+
loadState: statePath,
|
|
88
|
+
l1BlockTime: blockTimeS
|
|
89
|
+
});
|
|
90
|
+
const blockTime = BigInt(blockTimeS);
|
|
91
|
+
const nowS = BigInt(Math.ceil(cheat.dateProvider.now() / 1000));
|
|
92
|
+
const slotsSinceTip = nowS > tip.timestamp ? (nowS - tip.timestamp + blockTime - 1n) / blockTime : 1n;
|
|
93
|
+
await cheat.warp(tip.timestamp + blockTime * slotsSinceTip, {
|
|
94
|
+
silent: true,
|
|
95
|
+
resetBlockInterval: true
|
|
96
|
+
});
|
|
97
|
+
const forkBlock = await cheat.publicClient.getBlock({
|
|
98
|
+
blockNumber: tip.number + 1n
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
anvil: restarted.anvil,
|
|
102
|
+
forkBlock: {
|
|
103
|
+
number: forkBlock.number,
|
|
104
|
+
timestamp: forkBlock.timestamp
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|