@aztec/ethereum 0.0.1-commit.b6e433891 → 0.0.1-commit.b8a057fa
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 +17 -3
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +49 -3
- package/dest/config.d.ts +27 -8
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +59 -12
- 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/gse.d.ts +4 -2
- package/dest/contracts/gse.d.ts.map +1 -1
- package/dest/contracts/gse.js +2 -2
- 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 +130 -13
- package/dest/contracts/multicall.d.ts.map +1 -1
- package/dest/contracts/multicall.js +203 -94
- package/dest/contracts/outbox.d.ts +27 -4
- package/dest/contracts/outbox.d.ts.map +1 -1
- package/dest/contracts/outbox.js +34 -7
- package/dest/contracts/rollup.d.ts +5401 -26
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +343 -69
- 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 +18 -10
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +17 -13
- package/dest/l1_artifacts.d.ts +14941 -16273
- 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 +3 -5
- package/dest/l1_reader.d.ts.map +1 -1
- package/dest/l1_reader.js +3 -6
- package/dest/l1_tx_utils/config.js +3 -3
- package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +23 -4
- package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_fee_analyzer.js +65 -3
- 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 +138 -57
- 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 +2 -1
- 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/l1_tx_utils/types.d.ts +27 -1
- package/dest/l1_tx_utils/types.d.ts.map +1 -1
- package/dest/l1_tx_utils/types.js +10 -0
- package/dest/publisher_manager.d.ts +34 -8
- package/dest/publisher_manager.d.ts.map +1 -1
- package/dest/publisher_manager.js +119 -8
- package/dest/queries.d.ts +13 -2
- package/dest/queries.d.ts.map +1 -1
- package/dest/queries.js +69 -4
- package/dest/test/blob_kzg_warmup.d.ts +19 -0
- package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
- package/dest/test/blob_kzg_warmup.js +64 -0
- package/dest/test/chain_monitor.d.ts +34 -2
- package/dest/test/chain_monitor.d.ts.map +1 -1
- package/dest/test/chain_monitor.js +76 -6
- package/dest/test/eth_cheat_codes.d.ts +37 -6
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +89 -41
- package/dest/test/index.d.ts +2 -1
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +1 -0
- package/dest/test/rollup_cheat_codes.d.ts +60 -3
- package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
- package/dest/test/rollup_cheat_codes.js +86 -7
- package/dest/test/start_anvil.d.ts +8 -1
- package/dest/test/start_anvil.d.ts.map +1 -1
- package/dest/test/start_anvil.js +16 -2
- package/dest/utils.d.ts +9 -2
- package/dest/utils.d.ts.map +1 -1
- package/dest/utils.js +34 -6
- package/package.json +6 -7
- package/src/client.ts +57 -1
- package/src/config.ts +82 -16
- 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/gse.ts +7 -2
- package/src/contracts/inbox.ts +13 -5
- package/src/contracts/index.ts +2 -2
- package/src/contracts/multicall.ts +247 -106
- package/src/contracts/outbox.ts +42 -8
- package/src/contracts/rollup.ts +382 -69
- package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +14 -16
- package/src/deploy_aztec_l1_contracts.ts +16 -14
- package/src/l1_artifacts.ts +12 -35
- package/src/l1_contract_addresses.ts +120 -94
- package/src/l1_reader.ts +4 -9
- package/src/l1_tx_utils/config.ts +3 -3
- package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
- package/src/l1_tx_utils/l1_tx_utils.ts +122 -48
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +2 -1
- package/src/l1_tx_utils/tx_delayer.ts +16 -7
- package/src/l1_tx_utils/types.ts +32 -0
- package/src/publisher_manager.ts +143 -12
- package/src/queries.ts +76 -4
- package/src/test/blob_kzg_warmup.ts +65 -0
- package/src/test/chain_monitor.ts +93 -5
- package/src/test/eth_cheat_codes.ts +82 -45
- package/src/test/index.ts +1 -0
- package/src/test/rollup_cheat_codes.ts +115 -6
- package/src/test/start_anvil.ts +25 -2
- package/src/utils.ts +30 -6
- 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/dest/generated/l1-contracts-defaults.d.ts +0 -30
- package/dest/generated/l1-contracts-defaults.d.ts.map +0 -1
- package/dest/generated/l1-contracts-defaults.js +0 -30
- package/src/contracts/empire_slashing_proposer.ts +0 -265
- package/src/generated/l1-contracts-defaults.ts +0 -32
|
@@ -4,7 +4,8 @@ import { merge, pick } from '@aztec/foundation/collection';
|
|
|
4
4
|
import { InterruptError, TimeoutError } from '@aztec/foundation/error';
|
|
5
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
6
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
7
|
-
import {
|
|
7
|
+
import { Semaphore } from '@aztec/foundation/queue';
|
|
8
|
+
import { makeBackoff, retry, retryUntil } from '@aztec/foundation/retry';
|
|
8
9
|
import { sleep } from '@aztec/foundation/sleep';
|
|
9
10
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
11
|
import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
type L1TxConfig,
|
|
36
37
|
type L1TxRequest,
|
|
37
38
|
type L1TxState,
|
|
39
|
+
L1TxTimeoutError,
|
|
38
40
|
type SigningCallback,
|
|
39
41
|
TerminalTxUtilsState,
|
|
40
42
|
TxUtilsState,
|
|
@@ -43,10 +45,17 @@ import {
|
|
|
43
45
|
|
|
44
46
|
const MAX_L1_TX_STATES = 32;
|
|
45
47
|
|
|
48
|
+
// Backoff (in seconds) for retrying the read-only RPC calls that prepare a tx cancellation. A
|
|
49
|
+
// cancellation is fired in the background after a tx times out and is important (it frees the stuck
|
|
50
|
+
// nonce), so a transient RPC failure while reading the pending nonce or gas price must not abandon it.
|
|
51
|
+
const CANCELLATION_PREP_RETRY_BACKOFF_S = [1, 2, 4, 8];
|
|
52
|
+
|
|
46
53
|
export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
47
54
|
protected txs: L1TxState[] = [];
|
|
48
55
|
/** Last nonce successfully sent to the chain. Used as a lower bound when a fallback RPC node returns a stale count. */
|
|
49
56
|
private lastSentNonce: number | undefined;
|
|
57
|
+
/** Mutex to prevent concurrent sendTransaction calls from racing on the same nonce. */
|
|
58
|
+
private readonly sendMutex = new Semaphore(1);
|
|
50
59
|
/** Tx delayer for testing. Only set when enableDelayer config is true. */
|
|
51
60
|
public delayer?: Delayer;
|
|
52
61
|
/** KZG instance for blob operations. */
|
|
@@ -128,6 +137,14 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
128
137
|
);
|
|
129
138
|
}
|
|
130
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Clears the cached `lastSentNonce` so the next `sendTransaction` call fetches the real
|
|
142
|
+
* nonce from the chain. Call this after an L1 reorg to prevent stale nonce references.
|
|
143
|
+
*/
|
|
144
|
+
public resetNonce(): void {
|
|
145
|
+
this.lastSentNonce = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
131
148
|
public getSenderAddress() {
|
|
132
149
|
return this.address;
|
|
133
150
|
}
|
|
@@ -210,6 +227,19 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
210
227
|
return await this.signTransaction(txRequest as TransactionSerializable);
|
|
211
228
|
}
|
|
212
229
|
|
|
230
|
+
private async checkInterruptedOrTimedOut(gasConfig: Pick<L1TxConfig, 'txTimeoutAt'>): Promise<Date> {
|
|
231
|
+
if (this.interrupted) {
|
|
232
|
+
throw new InterruptError(`Transaction sending is interrupted`);
|
|
233
|
+
}
|
|
234
|
+
const now = new Date(await this.getL1Timestamp());
|
|
235
|
+
if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
|
|
236
|
+
throw new TimeoutError(
|
|
237
|
+
`Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return now;
|
|
241
|
+
}
|
|
242
|
+
|
|
213
243
|
/**
|
|
214
244
|
* Sends a transaction with gas estimation and pricing
|
|
215
245
|
* @param request - The transaction request (to, data, value)
|
|
@@ -222,14 +252,15 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
222
252
|
blobInputs?: L1BlobInputs,
|
|
223
253
|
stateChange: TxUtilsState = TxUtilsState.SENT,
|
|
224
254
|
): Promise<{ txHash: Hex; state: L1TxState }> {
|
|
225
|
-
if (this.interrupted) {
|
|
226
|
-
throw new InterruptError(`Transaction sending is interrupted`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
255
|
try {
|
|
230
256
|
const gasConfig = merge(this.config, gasConfigOverrides);
|
|
231
257
|
const account = this.getSenderAddress().toString();
|
|
232
258
|
|
|
259
|
+
// Fail fast before doing any work (gas estimation, balance check) if we've been interrupted
|
|
260
|
+
// or if the caller's deadline has already passed. The same check is repeated after gas
|
|
261
|
+
// estimation in case it took long enough to push us past the deadline.
|
|
262
|
+
await this.checkInterruptedOrTimedOut(gasConfig);
|
|
263
|
+
|
|
233
264
|
let gasLimit: bigint;
|
|
234
265
|
if (this.debugMaxGasLimit) {
|
|
235
266
|
gasLimit = MAX_L1_TX_LIMIT;
|
|
@@ -242,42 +273,41 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
242
273
|
|
|
243
274
|
const gasPrice = await this.getGasPrice(gasConfig, !!blobInputs);
|
|
244
275
|
|
|
245
|
-
|
|
246
|
-
throw new InterruptError(`Transaction sending is interrupted`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const now = new Date(await this.getL1Timestamp());
|
|
250
|
-
if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
|
|
251
|
-
throw new TimeoutError(
|
|
252
|
-
`Transaction timed out before sending (now ${now.toISOString()} > timeoutAt ${gasConfig.txTimeoutAt.toISOString()})`,
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const chainNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
|
|
257
|
-
// If a fallback RPC node returns a stale count (lower than what we last sent), use our
|
|
258
|
-
// local lower bound to avoid sending a duplicate of an already-pending transaction.
|
|
259
|
-
const nonce =
|
|
260
|
-
this.lastSentNonce !== undefined && chainNonce <= this.lastSentNonce ? this.lastSentNonce + 1 : chainNonce;
|
|
276
|
+
const now = await this.checkInterruptedOrTimedOut(gasConfig);
|
|
261
277
|
|
|
262
|
-
|
|
263
|
-
|
|
278
|
+
let txHash: Hex;
|
|
279
|
+
let nonce: number;
|
|
280
|
+
let baseState: Pick<L1TxState, 'request' | 'gasLimit' | 'blobInputs' | 'gasPrice' | 'nonce'>;
|
|
264
281
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
282
|
+
await this.sendMutex.acquire();
|
|
283
|
+
try {
|
|
284
|
+
const chainNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
|
|
285
|
+
// If a fallback RPC node returns a stale count (lower than what we last sent), use our
|
|
286
|
+
// local lower bound to avoid sending a duplicate of an already-pending transaction.
|
|
287
|
+
nonce =
|
|
288
|
+
this.lastSentNonce !== undefined && chainNonce <= this.lastSentNonce ? this.lastSentNonce + 1 : chainNonce;
|
|
289
|
+
|
|
290
|
+
baseState = { request, gasLimit, blobInputs, gasPrice, nonce };
|
|
291
|
+
const txData = this.makeTxData(baseState, { isCancelTx: false });
|
|
292
|
+
|
|
293
|
+
const signedRequest = await this.prepareSignedTransaction(txData);
|
|
294
|
+
txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
|
|
295
|
+
this.lastSentNonce = nonce;
|
|
296
|
+
} finally {
|
|
297
|
+
this.sendMutex.release();
|
|
298
|
+
}
|
|
270
299
|
|
|
271
300
|
// Create the new state for monitoring
|
|
272
301
|
const l1TxState: L1TxState = {
|
|
273
302
|
...baseState,
|
|
274
|
-
id: (await this.store?.consumeNextStateId(account)) ?? Math.max(...this.txs.map(tx => tx.id),
|
|
303
|
+
id: (await this.store?.consumeNextStateId(account)) ?? Math.max(...this.txs.map(tx => tx.id), -1) + 1,
|
|
275
304
|
txHashes: [txHash],
|
|
276
305
|
cancelTxHashes: [],
|
|
277
306
|
status: TxUtilsState.IDLE,
|
|
278
307
|
txConfigOverrides: gasConfigOverrides ?? {},
|
|
279
308
|
sentAtL1Ts: now,
|
|
280
309
|
lastSentAtL1Ts: now,
|
|
310
|
+
gasPriceHistory: [baseState.gasPrice],
|
|
281
311
|
};
|
|
282
312
|
|
|
283
313
|
// And persist it
|
|
@@ -408,10 +438,23 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
408
438
|
|
|
409
439
|
const initialTxHash = txHashes[0];
|
|
410
440
|
let currentTxHash = txHashes.at(-1)!;
|
|
411
|
-
let l1Timestamp
|
|
441
|
+
let l1Timestamp = 0;
|
|
412
442
|
|
|
413
443
|
while (true) {
|
|
414
|
-
|
|
444
|
+
try {
|
|
445
|
+
l1Timestamp = await this.getL1Timestamp();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
// A transient RPC failure here must not abort monitoring with a rejection: callers that
|
|
448
|
+
// fire this loop in the background would surface it as an unhandled rejection (e.g. when a
|
|
449
|
+
// test tears down its L1 node while a monitor iteration is in flight). Exit quietly if we
|
|
450
|
+
// are shutting down, otherwise retry on the next interval.
|
|
451
|
+
if (this.interrupted) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
this.logger.error(`Error fetching L1 timestamp while monitoring tx ${currentTxHash}`, err, { nonce, account });
|
|
455
|
+
await sleep(gasConfig.checkIntervalMs!);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
415
458
|
|
|
416
459
|
try {
|
|
417
460
|
const timePassed = l1Timestamp - state.lastSentAtL1Ts.getTime();
|
|
@@ -463,6 +506,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
463
506
|
if (timePassed >= stallTimeMs && attempts <= maxSpeedUpAttempts) {
|
|
464
507
|
const newGasPrice = await this.getGasPrice(gasConfig, isBlobTx, attempts, state.gasPrice);
|
|
465
508
|
state.gasPrice = newGasPrice;
|
|
509
|
+
state.gasPriceHistory?.push(newGasPrice);
|
|
466
510
|
|
|
467
511
|
this.logger.debug(
|
|
468
512
|
`Tx ${currentTxHash} with nonce ${nonce} from ${account} appears stuck. ` +
|
|
@@ -631,8 +675,22 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
631
675
|
blobInputs?: L1BlobInputs,
|
|
632
676
|
): Promise<{ receipt: TransactionReceipt; state: L1TxState }> {
|
|
633
677
|
const { state } = await this.sendTransaction(request, gasConfig, blobInputs);
|
|
634
|
-
|
|
635
|
-
|
|
678
|
+
try {
|
|
679
|
+
const receipt = await this.monitorTransaction(state);
|
|
680
|
+
return { receipt, state };
|
|
681
|
+
} catch (err) {
|
|
682
|
+
if (err instanceof TimeoutError) {
|
|
683
|
+
// Snapshot the ladder now: the fire-and-forget cancellation mutates state.gasPrice moments later.
|
|
684
|
+
throw new L1TxTimeoutError(err.message, {
|
|
685
|
+
gasPriceHistory: state.gasPriceHistory ? [...state.gasPriceHistory] : undefined,
|
|
686
|
+
finalGasPrice: state.gasPrice,
|
|
687
|
+
attempts: state.txHashes.length,
|
|
688
|
+
nonce: state.nonce,
|
|
689
|
+
gasLimit: state.gasLimit,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
throw err;
|
|
693
|
+
}
|
|
636
694
|
}
|
|
637
695
|
|
|
638
696
|
public override async simulate(
|
|
@@ -683,9 +741,37 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
683
741
|
return;
|
|
684
742
|
}
|
|
685
743
|
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
744
|
+
// Resolve the pending nonce and cancellation gas price up front. These are read-only RPC calls, so
|
|
745
|
+
// they are safe to retry; a transient RPC failure here must not permanently abandon the cancellation
|
|
746
|
+
// (which would leave the nonce stuck and the tx state stranded short of CANCELLED). We retry with a
|
|
747
|
+
// real backoff before giving up, unlike getGasPrice's tight internal retry which can be exhausted by
|
|
748
|
+
// a brief node hiccup. The retried block performs no state-changing send, so retrying cannot
|
|
749
|
+
// double-send the cancellation tx.
|
|
750
|
+
const { currentNonce, cancelGasPrice } = await retry(
|
|
751
|
+
async () => {
|
|
752
|
+
const currentNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
|
|
753
|
+
if (currentNonce >= nonce) {
|
|
754
|
+
// Get gas price with higher priority fee for cancellation
|
|
755
|
+
const cancelGasPrice = await this.getGasPrice(
|
|
756
|
+
{
|
|
757
|
+
...this.config,
|
|
758
|
+
// Use high bump for cancellation to ensure it replaces the original tx
|
|
759
|
+
priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
|
|
760
|
+
},
|
|
761
|
+
isBlobTx,
|
|
762
|
+
state.txHashes.length,
|
|
763
|
+
previousGasPrice,
|
|
764
|
+
);
|
|
765
|
+
return { currentNonce, cancelGasPrice };
|
|
766
|
+
}
|
|
767
|
+
return { currentNonce, cancelGasPrice: undefined };
|
|
768
|
+
},
|
|
769
|
+
`Preparing cancellation for L1 tx from account ${account} with nonce ${nonce}`,
|
|
770
|
+
makeBackoff(CANCELLATION_PREP_RETRY_BACKOFF_S),
|
|
771
|
+
this.logger,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
if (cancelGasPrice === undefined) {
|
|
689
775
|
this.logger.verbose(
|
|
690
776
|
`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as it is dropped`,
|
|
691
777
|
{ nonce, account, currentNonce },
|
|
@@ -694,18 +780,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
694
780
|
return;
|
|
695
781
|
}
|
|
696
782
|
|
|
697
|
-
// Get gas price with higher priority fee for cancellation
|
|
698
|
-
const cancelGasPrice = await this.getGasPrice(
|
|
699
|
-
{
|
|
700
|
-
...this.config,
|
|
701
|
-
// Use high bump for cancellation to ensure it replaces the original tx
|
|
702
|
-
priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
|
|
703
|
-
},
|
|
704
|
-
isBlobTx,
|
|
705
|
-
state.txHashes.length,
|
|
706
|
-
previousGasPrice,
|
|
707
|
-
);
|
|
708
|
-
|
|
709
783
|
const { maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = cancelGasPrice;
|
|
710
784
|
this.logger.verbose(
|
|
711
785
|
`Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction from account ${account} with nonce ${nonce} after time out`,
|
|
@@ -3,6 +3,7 @@ import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
|
3
3
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
4
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
5
5
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
6
|
+
import { getErrorCause } from '@aztec/foundation/types';
|
|
6
7
|
import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
|
|
7
8
|
|
|
8
9
|
import pickBy from 'lodash.pickby';
|
|
@@ -395,7 +396,7 @@ export class ReadOnlyL1TxUtils {
|
|
|
395
396
|
this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
|
|
396
397
|
return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
|
|
397
398
|
} catch (err) {
|
|
398
|
-
if (err
|
|
399
|
+
if (getErrorCause(err, MethodNotFoundRpcError) || getErrorCause(err, MethodNotSupportedRpcError)) {
|
|
399
400
|
if (gasConfig.fallbackGasEstimate) {
|
|
400
401
|
this.logger?.warn(
|
|
401
402
|
`Node does not support eth_simulateV1 API. Using fallback gas estimate: ${gasConfig.fallbackGasEstimate}`,
|
|
@@ -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/l1_tx_utils/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlobKzgInstance } from '@aztec/blob-lib/types';
|
|
2
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
2
3
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
4
|
import type { ViemTransactionSignature } from '@aztec/foundation/eth-signature';
|
|
4
5
|
|
|
@@ -55,6 +56,12 @@ export type L1TxState = {
|
|
|
55
56
|
cancelTxHashes: Hex[];
|
|
56
57
|
gasLimit: bigint;
|
|
57
58
|
gasPrice: GasPrice;
|
|
59
|
+
/**
|
|
60
|
+
* Prices used for each attempt (initial send followed by each speed-up), in order. Always set on
|
|
61
|
+
* newly sent txs; optional because states restored from the state store predate the field.
|
|
62
|
+
* In-memory only — not persisted by the state store.
|
|
63
|
+
*/
|
|
64
|
+
gasPriceHistory?: GasPrice[];
|
|
58
65
|
txConfigOverrides: L1TxConfig;
|
|
59
66
|
request: L1TxRequest;
|
|
60
67
|
status: TxUtilsState;
|
|
@@ -83,3 +90,28 @@ export class DroppedTransactionError extends Error {
|
|
|
83
90
|
this.name = 'DroppedTransactionError';
|
|
84
91
|
}
|
|
85
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Snapshot of what a timed-out L1 tx tried to pay, taken when the timeout is raised. */
|
|
95
|
+
export type TimedOutTxState = {
|
|
96
|
+
/** Prices used across the initial send and each speed-up, in order (undefined only for restored states). */
|
|
97
|
+
gasPriceHistory?: GasPrice[];
|
|
98
|
+
/** The last price the tx was sent at before timing out. */
|
|
99
|
+
finalGasPrice: GasPrice;
|
|
100
|
+
/** Number of send attempts (initial + speed-ups). */
|
|
101
|
+
attempts: number;
|
|
102
|
+
nonce: number;
|
|
103
|
+
gasLimit: bigint;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Thrown by sendAndMonitorTransaction when a tx times out. Subclasses TimeoutError so existing
|
|
108
|
+
* `instanceof TimeoutError` checks keep working, while carrying the gas-price ladder for diagnostics.
|
|
109
|
+
*/
|
|
110
|
+
export class L1TxTimeoutError extends TimeoutError {
|
|
111
|
+
constructor(
|
|
112
|
+
message: string,
|
|
113
|
+
public readonly txState: TimedOutTxState,
|
|
114
|
+
) {
|
|
115
|
+
super(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
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,99 @@ 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
|
+
protected funder?: UtilsType;
|
|
44
|
+
protected readonly fundingPromise?: RunningPromise;
|
|
45
|
+
private started = false;
|
|
33
46
|
|
|
34
47
|
constructor(
|
|
35
|
-
|
|
36
|
-
config:
|
|
37
|
-
bindings?: LoggerBindings,
|
|
48
|
+
protected publishers: UtilsType[],
|
|
49
|
+
config: PublisherManagerConfig,
|
|
50
|
+
opts?: { bindings?: LoggerBindings; funder?: UtilsType },
|
|
38
51
|
) {
|
|
39
|
-
this.
|
|
52
|
+
this.funder = opts?.funder;
|
|
53
|
+
this.log = createLogger('publisher:manager', opts?.bindings);
|
|
40
54
|
this.log.info(`PublisherManager initialized with ${publishers.length} publishers.`);
|
|
41
55
|
this.publishers = publishers;
|
|
42
|
-
this.config = pick(config, 'publisherAllowInvalidStates');
|
|
56
|
+
this.config = pick(config, 'publisherAllowInvalidStates', 'publisherFundingThreshold', 'publisherFundingAmount');
|
|
57
|
+
|
|
58
|
+
const hasThreshold = this.config.publisherFundingThreshold !== undefined;
|
|
59
|
+
const hasAmount = this.config.publisherFundingAmount !== undefined;
|
|
60
|
+
if (hasThreshold !== hasAmount) {
|
|
61
|
+
this.log.warn(`Incomplete funding config: both publisherFundingThreshold and publisherFundingAmount must be set`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (this.funder) {
|
|
65
|
+
const funderAddress = this.funder.getSenderAddress();
|
|
66
|
+
if (publishers.some(p => p.getSenderAddress().equals(funderAddress))) {
|
|
67
|
+
this.log.error(`Funding account ${funderAddress} is also a publisher, disabling funding to avoid self-funding`);
|
|
68
|
+
this.funder = undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this.funder && hasThreshold && hasAmount) {
|
|
73
|
+
this.fundingPromise = new RunningPromise(
|
|
74
|
+
() => this.triggerFundingIfNeeded(),
|
|
75
|
+
this.log,
|
|
76
|
+
PublisherManager.FUNDING_CHECK_INTERVAL_MS,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clears any interrupted flag left by a previous {@link stop} so publishing works again after a restart,
|
|
83
|
+
* loads the state of all publishers and the funder, and starts periodic funding checks. Idempotent: a
|
|
84
|
+
* start while already started is a no-op, so it never re-runs `loadStateAndResumeMonitoring` (which
|
|
85
|
+
* would spawn a duplicate background monitor per pending nonce). Lifecycle calls are expected to be
|
|
86
|
+
* serialized by the caller.
|
|
87
|
+
*/
|
|
88
|
+
public async start(): Promise<void> {
|
|
89
|
+
if (this.started) {
|
|
90
|
+
this.log.debug('PublisherManager already started, ignoring start');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clear the interrupted flag set by a previous stop() so a restarted manager can publish again.
|
|
95
|
+
// On a first start this is a no-op (the flag is already clear).
|
|
96
|
+
this.publishers.forEach(pub => pub.restart());
|
|
97
|
+
this.funder?.restart();
|
|
98
|
+
|
|
99
|
+
await Promise.all([
|
|
100
|
+
...this.publishers.map(pub => pub.loadStateAndResumeMonitoring()),
|
|
101
|
+
this.funder?.loadStateAndResumeMonitoring(),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
this.fundingPromise?.start();
|
|
105
|
+
// Marked started only once fully up, so a start that failed to load state can be retried.
|
|
106
|
+
this.started = true;
|
|
43
107
|
}
|
|
44
108
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Stops the funding loop, interrupts all publishers so no further L1 txs are sent, and waits (bounded)
|
|
111
|
+
* for their in-flight tx monitor loops to wind down. Idempotent, and the manager may be restarted
|
|
112
|
+
* afterwards via {@link start}, which clears the interrupted flag.
|
|
113
|
+
*/
|
|
114
|
+
public async stop(): Promise<void> {
|
|
115
|
+
this.started = false;
|
|
116
|
+
await this.fundingPromise?.stop();
|
|
117
|
+
this.publishers.forEach(pub => pub.interrupt());
|
|
118
|
+
this.funder?.interrupt();
|
|
119
|
+
// Wait for in-flight tx monitor loops to observe the interrupt, so no L1 requests are still
|
|
120
|
+
// being issued after shutdown (e.g. against an anvil instance a test is about to tear down).
|
|
121
|
+
await Promise.all([
|
|
122
|
+
...this.publishers.map(pub => pub.waitMonitoringStopped()),
|
|
123
|
+
this.funder?.waitMonitoringStopped(),
|
|
124
|
+
]);
|
|
48
125
|
}
|
|
49
126
|
|
|
50
127
|
// Finds and prioritises available publishers based on
|
|
@@ -83,8 +160,17 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
|
|
|
83
160
|
}),
|
|
84
161
|
);
|
|
85
162
|
|
|
163
|
+
// Discount unfunded publishers: a publisher with no ETH cannot send a tx, so it must never win
|
|
164
|
+
// selection regardless of how favourable its state or last-used time is. Fall back to the full
|
|
165
|
+
// set only if every candidate is unfunded, so behaviour is no worse than before.
|
|
166
|
+
let fundedPublishers = publishersWithBalance.filter(p => p.balance > 0n);
|
|
167
|
+
if (fundedPublishers.length === 0) {
|
|
168
|
+
this.log.warn(`All candidate publishers have zero balance; selecting from unfunded publishers.`);
|
|
169
|
+
fundedPublishers = publishersWithBalance;
|
|
170
|
+
}
|
|
171
|
+
|
|
86
172
|
// Sort based on state, then balance, then time since last use
|
|
87
|
-
const sortedPublishers =
|
|
173
|
+
const sortedPublishers = fundedPublishers.sort((a, b) => {
|
|
88
174
|
const stateComparison = sortOrder.indexOf(a.publisher.state) - sortOrder.indexOf(b.publisher.state);
|
|
89
175
|
if (stateComparison !== 0) {
|
|
90
176
|
return stateComparison;
|
|
@@ -102,7 +188,52 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
|
|
|
102
188
|
return sortedPublishers[0].publisher;
|
|
103
189
|
}
|
|
104
190
|
|
|
105
|
-
|
|
106
|
-
|
|
191
|
+
/** Check all publisher balances and fund those below threshold. */
|
|
192
|
+
private async triggerFundingIfNeeded(): Promise<void> {
|
|
193
|
+
const { funder, config } = this;
|
|
194
|
+
if (!funder || config.publisherFundingThreshold === undefined || config.publisherFundingAmount === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const allBalances = await Promise.all(
|
|
199
|
+
this.publishers.map(async pub => ({ balance: await pub.getSenderBalance(), publisher: pub })),
|
|
200
|
+
);
|
|
201
|
+
const lowBalance = allBalances.filter(p => p.balance < config.publisherFundingThreshold!);
|
|
202
|
+
if (lowBalance.length === 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fundingAmount = config.publisherFundingAmount!;
|
|
207
|
+
const funderBalance = await funder.getSenderBalance();
|
|
208
|
+
|
|
209
|
+
if (funderBalance < 10n * fundingAmount) {
|
|
210
|
+
this.log.warn(`Funding account balance is low`, { funderBalance, threshold: 10n * fundingAmount });
|
|
211
|
+
}
|
|
212
|
+
const affordableCount = Number(funderBalance / fundingAmount);
|
|
213
|
+
if (affordableCount === 0) {
|
|
214
|
+
this.log.error(`Funding account balance too low to fund any publisher`, { funderBalance, fundingAmount });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (affordableCount < lowBalance.length) {
|
|
218
|
+
this.log.warn(`Funder can only afford ${affordableCount}/${lowBalance.length} publishers`, {
|
|
219
|
+
funderBalance,
|
|
220
|
+
fundingAmount,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const toFund = lowBalance.slice(0, affordableCount).map(p => p.publisher);
|
|
225
|
+
await this.fundPublishers(toFund);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Fund publishers via a single Multicall3 aggregate3Value transaction. */
|
|
229
|
+
private async fundPublishers(publishers: UtilsType[]): Promise<void> {
|
|
230
|
+
const fundingAmount = this.config.publisherFundingAmount!;
|
|
231
|
+
const calls = publishers.map(pub => ({
|
|
232
|
+
to: pub.getSenderAddress().toString(),
|
|
233
|
+
value: fundingAmount,
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
await Multicall3.forwardValue(calls, this.funder!, this.log);
|
|
237
|
+
this.log.info(`Funded ${publishers.length} publishers`);
|
|
107
238
|
}
|
|
108
239
|
}
|