@aztec/ethereum 0.0.1-commit.f504929 → 0.0.1-commit.f5a9928
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 +27 -3
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +63 -8
- package/dest/config.d.ts +15 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +29 -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 +135 -35
- package/dest/contracts/governance.d.ts.map +1 -1
- package/dest/contracts/governance.js +226 -15
- package/dest/contracts/governance_proposer.d.ts +33 -4
- package/dest/contracts/governance_proposer.d.ts.map +1 -1
- package/dest/contracts/governance_proposer.js +52 -13
- 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 +127 -12
- package/dest/contracts/multicall.d.ts.map +1 -1
- package/dest/contracts/multicall.js +201 -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 +5433 -30
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +436 -102
- package/dest/contracts/slashing_proposer.d.ts +171 -0
- package/dest/contracts/slashing_proposer.d.ts.map +1 -0
- package/dest/contracts/slashing_proposer.js +753 -0
- package/dest/contracts/watch_event.d.ts +36 -0
- package/dest/contracts/watch_event.d.ts.map +1 -0
- package/dest/contracts/watch_event.js +66 -0
- package/dest/deploy_aztec_l1_contracts.d.ts +4 -7
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +15 -8
- package/dest/foundry_binary.d.ts +13 -0
- package/dest/foundry_binary.d.ts.map +1 -0
- package/dest/foundry_binary.js +52 -0
- package/dest/generated/l1-contracts-defaults.d.ts +2 -2
- package/dest/generated/l1-contracts-defaults.js +2 -2
- 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 +5 -5
- package/dest/l1_reader.d.ts.map +1 -1
- package/dest/l1_reader.js +9 -7
- package/dest/l1_tx_utils/config.js +3 -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 +115 -54
- 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 +57 -23
- 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 +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 +62 -3
- 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 +55 -4
- package/dest/test/chain_monitor.d.ts.map +1 -1
- package/dest/test/chain_monitor.js +104 -3
- package/dest/test/eth_cheat_codes.d.ts +31 -6
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +67 -44
- 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/recording_rpc_server.d.ts +18 -0
- package/dest/test/recording_rpc_server.d.ts.map +1 -0
- package/dest/test/recording_rpc_server.js +68 -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 +15 -1
- package/dest/test/start_anvil.d.ts.map +1 -1
- package/dest/test/start_anvil.js +58 -7
- package/dest/utils.d.ts +9 -2
- package/dest/utils.d.ts.map +1 -1
- package/dest/utils.js +59 -24
- package/package.json +6 -6
- package/src/client.ts +72 -2
- package/src/config.ts +44 -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 +310 -10
- package/src/contracts/governance_proposer.ts +56 -13
- 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 +243 -105
- package/src/contracts/outbox.ts +42 -8
- package/src/contracts/rollup.ts +487 -103
- package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +119 -45
- package/src/contracts/watch_event.ts +110 -0
- package/src/deploy_aztec_l1_contracts.ts +19 -10
- package/src/foundry_binary.ts +57 -0
- package/src/generated/l1-contracts-defaults.ts +2 -2
- 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/config.ts +3 -3
- package/src/l1_tx_utils/l1_tx_utils.ts +107 -50
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +57 -17
- package/src/l1_tx_utils/tx_delayer.ts +16 -7
- package/src/publisher_manager.ts +143 -12
- package/src/queries.ts +70 -4
- package/src/test/blob_kzg_warmup.ts +65 -0
- package/src/test/chain_monitor.ts +148 -3
- package/src/test/eth_cheat_codes.ts +63 -47
- package/src/test/index.ts +1 -0
- package/src/test/recording_rpc_server.ts +69 -0
- package/src/test/rollup_cheat_codes.ts +115 -6
- package/src/test/start_anvil.ts +71 -8
- package/src/utils.ts +56 -29
- 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 +0 -140
- package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
- package/dest/contracts/tally_slashing_proposer.js +0 -320
- package/src/contracts/empire_slashing_proposer.ts +0 -265
|
@@ -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';
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
type Hex,
|
|
17
18
|
MethodNotFoundRpcError,
|
|
18
19
|
MethodNotSupportedRpcError,
|
|
20
|
+
RpcRequestError,
|
|
19
21
|
type StateOverride,
|
|
20
22
|
decodeErrorResult,
|
|
21
23
|
formatGwei,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
hexToBytes,
|
|
24
26
|
} from 'viem';
|
|
25
27
|
|
|
28
|
+
import { getL1RpcErrorCode } from '../client.js';
|
|
26
29
|
import type { ViemClient } from '../types.js';
|
|
27
30
|
import { type L1TxUtilsConfig, defaultL1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
|
|
28
31
|
import {
|
|
@@ -39,6 +42,23 @@ import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './ut
|
|
|
39
42
|
// Change this to the current strategy we want to use
|
|
40
43
|
const CurrentStrategy: PriorityFeeStrategy = P75AllTxsPriorityFeeStrategy;
|
|
41
44
|
|
|
45
|
+
/** Error code returned by eth_simulateV1 for `insufficient funds for gas * price + value`. */
|
|
46
|
+
const INSUFFICIENT_FUNDS_RPC_ERROR_CODE = -38014;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true when an RPC transport error reports the node's upfront funds check
|
|
50
|
+
* (sender balance >= gasLimit * maxFeePerGas) rejecting the request. Matches on the message as well as on the
|
|
51
|
+
* error code, since RPC gateways differ in how they wrap node errors. Apply this only to transport errors: an
|
|
52
|
+
* execution revert string can mention insufficient funds without being this kind of rejection.
|
|
53
|
+
*/
|
|
54
|
+
function isInsufficientFundsRpcError(err: unknown): boolean {
|
|
55
|
+
if (getL1RpcErrorCode(err) === INSUFFICIENT_FUNDS_RPC_ERROR_CODE) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const messages = [err, getErrorCause(err, RpcRequestError)].map(e => (e instanceof Error ? e.message : '')).join(' ');
|
|
59
|
+
return /insufficient funds/i.test(messages);
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
export class ReadOnlyL1TxUtils {
|
|
43
63
|
public config: Required<L1TxUtilsConfig>;
|
|
44
64
|
protected interrupted = false;
|
|
@@ -130,9 +150,10 @@ export class ReadOnlyL1TxUtils {
|
|
|
130
150
|
const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS);
|
|
131
151
|
for (let i = 0; i < numBlocks; i++) {
|
|
132
152
|
// each block can go up 12.5% from previous baseFee
|
|
133
|
-
|
|
153
|
+
// ceil, (a+b-1)/b, to avoid truncation at small values (e.g. 1 wei blob base fee)
|
|
154
|
+
maxFeePerGas = (maxFeePerGas * (1_000n + 125n) + 999n) / 1_000n;
|
|
134
155
|
// same for blob gas fee
|
|
135
|
-
maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n)) / 1_000n;
|
|
156
|
+
maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n) + 999n) / 1_000n;
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
if (attempt > 0) {
|
|
@@ -242,13 +263,16 @@ export class ReadOnlyL1TxUtils {
|
|
|
242
263
|
const gasConfig = { ...this.config, ..._gasConfig };
|
|
243
264
|
let initialEstimate = 0n;
|
|
244
265
|
if (_blobInputs) {
|
|
245
|
-
// @note requests with blobs also require maxFeePerBlobGas to be set
|
|
266
|
+
// @note requests with blobs also require maxFeePerBlobGas to be set.
|
|
267
|
+
// Use 2x buffer for maxFeePerBlobGas to avoid stale fees and to pass EIP-4844 validation (even if it is a gas estimation call).
|
|
268
|
+
// 1. maxFeePerBlobGas >= blobBaseFee
|
|
269
|
+
// 2. account balance >= gas * maxFeePerGas + maxFeePerBlobGas * blobCount + value
|
|
246
270
|
const gasPrice = await this.getGasPrice(gasConfig, true, 0);
|
|
247
271
|
initialEstimate = await this.client.estimateGas({
|
|
248
272
|
account,
|
|
249
273
|
...request,
|
|
250
274
|
..._blobInputs,
|
|
251
|
-
maxFeePerBlobGas: gasPrice.maxFeePerBlobGas
|
|
275
|
+
maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! * 2n,
|
|
252
276
|
gas: MAX_L1_TX_LIMIT,
|
|
253
277
|
blockTag: 'latest',
|
|
254
278
|
});
|
|
@@ -368,8 +392,8 @@ export class ReadOnlyL1TxUtils {
|
|
|
368
392
|
gasConfig: L1TxUtilsConfig & { fallbackGasEstimate?: bigint },
|
|
369
393
|
abi: Abi,
|
|
370
394
|
) {
|
|
371
|
-
|
|
372
|
-
|
|
395
|
+
const simulateBlocks = () =>
|
|
396
|
+
this.client.simulateBlocks({
|
|
373
397
|
validation: false,
|
|
374
398
|
blocks: [
|
|
375
399
|
{
|
|
@@ -380,18 +404,13 @@ export class ReadOnlyL1TxUtils {
|
|
|
380
404
|
],
|
|
381
405
|
});
|
|
382
406
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
`L1 transaction simulation failed with error ${decodedError.errorName}(${decodedError.args?.join(',')})`,
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
|
|
392
|
-
return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
|
|
407
|
+
// Only the request itself is wrapped, so that transport-level rejections are never confused with an
|
|
408
|
+
// execution revert (whose decoded revert string could well mention insufficient funds too).
|
|
409
|
+
let result: Awaited<ReturnType<typeof simulateBlocks>>;
|
|
410
|
+
try {
|
|
411
|
+
result = await simulateBlocks();
|
|
393
412
|
} catch (err) {
|
|
394
|
-
if (err
|
|
413
|
+
if (getErrorCause(err, MethodNotFoundRpcError) || getErrorCause(err, MethodNotSupportedRpcError)) {
|
|
395
414
|
if (gasConfig.fallbackGasEstimate) {
|
|
396
415
|
this.logger?.warn(
|
|
397
416
|
`Node does not support eth_simulateV1 API. Using fallback gas estimate: ${gasConfig.fallbackGasEstimate}`,
|
|
@@ -400,8 +419,29 @@ export class ReadOnlyL1TxUtils {
|
|
|
400
419
|
}
|
|
401
420
|
this.logger?.error('Node does not support eth_simulateV1 API');
|
|
402
421
|
}
|
|
422
|
+
if (isInsufficientFundsRpcError(err)) {
|
|
423
|
+
const rpcError = getErrorCause(err, RpcRequestError);
|
|
424
|
+
const providerMessage = rpcError?.details ?? (err instanceof Error ? err.message : String(err));
|
|
425
|
+
const gas = call?.gas !== undefined ? `, gas ${call.gas}` : '';
|
|
426
|
+
throw new Error(
|
|
427
|
+
`L1 node rejected the eth_simulateV1 request with an insufficient funds error before executing it ` +
|
|
428
|
+
`(sender ${call?.from ?? 'unknown'}${gas}): ${providerMessage}`,
|
|
429
|
+
{ cause: err },
|
|
430
|
+
);
|
|
431
|
+
}
|
|
403
432
|
throw err;
|
|
404
433
|
}
|
|
434
|
+
|
|
435
|
+
if (result[0].calls[0].status === 'failure') {
|
|
436
|
+
this.logger?.error('L1 transaction simulation failed', result[0].calls[0].error);
|
|
437
|
+
const decodedError = decodeErrorResult({ abi, data: result[0].calls[0].data });
|
|
438
|
+
|
|
439
|
+
throw new Error(
|
|
440
|
+
`L1 transaction simulation failed with error ${decodedError.errorName}(${decodedError.args?.join(',')})`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
|
|
444
|
+
return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
|
|
405
445
|
}
|
|
406
446
|
|
|
407
447
|
public bumpGasLimit(gasLimit: bigint, _gasConfig?: L1TxUtilsConfig): bigint {
|
|
@@ -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,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
|
}
|
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],
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getBytesPerBlob, getBytesPerCommitment, getKzg } from '@aztec/blob-lib';
|
|
2
|
+
import type { Logger } from '@aztec/foundation/log';
|
|
3
|
+
|
|
4
|
+
import type { ExtendedViemWalletClient } from '../types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Warms both KZG trusted setups in parallel for tests against a local anvil:
|
|
8
|
+
* - anvil's own setup, loaded lazily when it first validates a blob sidecar in `eth_sendRawTransaction`
|
|
9
|
+
* - our `@crate-crypto/node-eth-kzg` singleton (`getKzg()`, a ~2.2s synchronous precomp build)
|
|
10
|
+
*
|
|
11
|
+
* Without this, the first checkpoint publish pays both serially (~4.4s). The recipe does all RPC round
|
|
12
|
+
* trips up front, fires a single bare raw send carrying a fake (garbage) blob sidecar that anvil rejects
|
|
13
|
+
* after loading its trusted setup, yields once to flush the socket write, then synchronously builds our
|
|
14
|
+
* own precomp tables while anvil warms in parallel. The fake tx is rejected at admission, so it never
|
|
15
|
+
* enters the pool, mines no block, and burns no nonce — sending it from the passed client is harmless.
|
|
16
|
+
*
|
|
17
|
+
* Best-effort: any failure is debug-logged and swallowed, leaving lazy init as the fallback. Never throws.
|
|
18
|
+
*
|
|
19
|
+
* @param l1Client - The L1 client whose account sends the (rejected) warm-up blob tx.
|
|
20
|
+
*/
|
|
21
|
+
export async function warmBlobKzg(l1Client: ExtendedViemWalletClient, logger?: Logger): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
// A pure-JS fake kzg: returns deterministic garbage of the correct lengths (commitment and proof are
|
|
24
|
+
// both 48 bytes). It makes viem build a syntactically valid sidecar without initializing any native
|
|
25
|
+
// module, so anvil loads its trusted setup to validate the (invalid) proof and then rejects the tx.
|
|
26
|
+
const fakeKzg = {
|
|
27
|
+
blobToKzgCommitment: () => {
|
|
28
|
+
const commitment = new Uint8Array(getBytesPerCommitment());
|
|
29
|
+
commitment.fill(0xab);
|
|
30
|
+
commitment[0] = 0xc0;
|
|
31
|
+
return commitment;
|
|
32
|
+
},
|
|
33
|
+
computeBlobKzgProof: () => {
|
|
34
|
+
const proof = new Uint8Array(getBytesPerCommitment());
|
|
35
|
+
proof.fill(0xcd);
|
|
36
|
+
proof[0] = 0xc0;
|
|
37
|
+
return proof;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// All RPC round trips (nonce, fees, gas) happen here, so only a single socket write remains to flush.
|
|
42
|
+
const prepared = await l1Client.prepareTransactionRequest({
|
|
43
|
+
blobs: [new Uint8Array(getBytesPerBlob())],
|
|
44
|
+
kzg: fakeKzg,
|
|
45
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
46
|
+
value: 0n,
|
|
47
|
+
maxFeePerBlobGas: 1_000_000_000_000n,
|
|
48
|
+
});
|
|
49
|
+
const serialized = await l1Client.signTransaction(prepared);
|
|
50
|
+
|
|
51
|
+
// Fire the raw send without awaiting and swallow the expected rejection.
|
|
52
|
+
const sent = l1Client.request({ method: 'eth_sendRawTransaction', params: [serialized] }).catch(() => {});
|
|
53
|
+
|
|
54
|
+
// Flush the single socket write so anvil receives the tx and starts loading its trusted setup in its
|
|
55
|
+
// own process before we block the event loop below.
|
|
56
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
57
|
+
|
|
58
|
+
// Synchronously build our precomp tables (~2.2s, event-loop-blocking); anvil warms in parallel.
|
|
59
|
+
getKzg();
|
|
60
|
+
|
|
61
|
+
await sent;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
logger?.debug('Failed to warm blob KZG; falling back to lazy init', { err });
|
|
64
|
+
}
|
|
65
|
+
}
|