@aztec/ethereum 0.0.1-commit.993d52e → 0.0.1-commit.9a89641

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.
Files changed (145) hide show
  1. package/dest/client.d.ts +25 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +60 -8
  4. package/dest/config.d.ts +27 -8
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +60 -13
  7. package/dest/contracts/chain_state_override.d.ts +78 -0
  8. package/dest/contracts/chain_state_override.d.ts.map +1 -0
  9. package/dest/contracts/chain_state_override.js +137 -0
  10. package/dest/contracts/empire_base.d.ts +1 -3
  11. package/dest/contracts/empire_base.d.ts.map +1 -1
  12. package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
  13. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
  14. package/dest/contracts/fee_asset_price_oracle.js +11 -6
  15. package/dest/contracts/governance.d.ts +135 -35
  16. package/dest/contracts/governance.d.ts.map +1 -1
  17. package/dest/contracts/governance.js +226 -15
  18. package/dest/contracts/governance_proposer.d.ts +33 -4
  19. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  20. package/dest/contracts/governance_proposer.js +52 -13
  21. package/dest/contracts/gse.d.ts +8 -2
  22. package/dest/contracts/gse.d.ts.map +1 -1
  23. package/dest/contracts/gse.js +11 -2
  24. package/dest/contracts/inbox.d.ts +3 -3
  25. package/dest/contracts/inbox.d.ts.map +1 -1
  26. package/dest/contracts/inbox.js +12 -7
  27. package/dest/contracts/index.d.ts +3 -3
  28. package/dest/contracts/index.d.ts.map +1 -1
  29. package/dest/contracts/index.js +2 -2
  30. package/dest/contracts/multicall.d.ts +130 -13
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +203 -94
  33. package/dest/contracts/outbox.d.ts +27 -4
  34. package/dest/contracts/outbox.d.ts.map +1 -1
  35. package/dest/contracts/outbox.js +34 -7
  36. package/dest/contracts/rollup.d.ts +5423 -27
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +396 -73
  39. package/dest/contracts/slashing_proposer.d.ts +170 -0
  40. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  41. package/dest/contracts/slashing_proposer.js +748 -0
  42. package/dest/deploy_aztec_l1_contracts.d.ts +18 -10
  43. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  44. package/dest/deploy_aztec_l1_contracts.js +28 -17
  45. package/dest/foundry_binary.d.ts +13 -0
  46. package/dest/foundry_binary.d.ts.map +1 -0
  47. package/dest/foundry_binary.js +52 -0
  48. package/dest/l1_artifacts.d.ts +14943 -16270
  49. package/dest/l1_artifacts.d.ts.map +1 -1
  50. package/dest/l1_artifacts.js +9 -24
  51. package/dest/l1_contract_addresses.d.ts +61 -65
  52. package/dest/l1_contract_addresses.d.ts.map +1 -1
  53. package/dest/l1_contract_addresses.js +101 -82
  54. package/dest/l1_reader.d.ts +5 -5
  55. package/dest/l1_reader.d.ts.map +1 -1
  56. package/dest/l1_reader.js +9 -7
  57. package/dest/l1_tx_utils/config.js +3 -3
  58. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +23 -4
  59. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -1
  60. package/dest/l1_tx_utils/l1_fee_analyzer.js +65 -3
  61. package/dest/l1_tx_utils/l1_tx_utils.d.ts +9 -1
  62. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  63. package/dest/l1_tx_utils/l1_tx_utils.js +138 -57
  64. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  65. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  66. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +10 -5
  67. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  68. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  69. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  70. package/dest/l1_tx_utils/types.d.ts +27 -1
  71. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  72. package/dest/l1_tx_utils/types.js +10 -0
  73. package/dest/publisher_manager.d.ts +34 -8
  74. package/dest/publisher_manager.d.ts.map +1 -1
  75. package/dest/publisher_manager.js +119 -8
  76. package/dest/queries.d.ts +13 -2
  77. package/dest/queries.d.ts.map +1 -1
  78. package/dest/queries.js +69 -4
  79. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  80. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  81. package/dest/test/blob_kzg_warmup.js +64 -0
  82. package/dest/test/chain_monitor.d.ts +55 -4
  83. package/dest/test/chain_monitor.d.ts.map +1 -1
  84. package/dest/test/chain_monitor.js +104 -3
  85. package/dest/test/eth_cheat_codes.d.ts +37 -6
  86. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  87. package/dest/test/eth_cheat_codes.js +89 -41
  88. package/dest/test/index.d.ts +2 -1
  89. package/dest/test/index.d.ts.map +1 -1
  90. package/dest/test/index.js +1 -0
  91. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  92. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  93. package/dest/test/rollup_cheat_codes.js +86 -7
  94. package/dest/test/start_anvil.d.ts +23 -3
  95. package/dest/test/start_anvil.d.ts.map +1 -1
  96. package/dest/test/start_anvil.js +182 -32
  97. package/dest/utils.d.ts +9 -2
  98. package/dest/utils.d.ts.map +1 -1
  99. package/dest/utils.js +48 -16
  100. package/package.json +6 -9
  101. package/src/client.ts +66 -2
  102. package/src/config.ts +83 -17
  103. package/src/contracts/chain_state_override.ts +200 -0
  104. package/src/contracts/empire_base.ts +0 -2
  105. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  106. package/src/contracts/governance.ts +310 -10
  107. package/src/contracts/governance_proposer.ts +56 -13
  108. package/src/contracts/gse.ts +19 -2
  109. package/src/contracts/inbox.ts +13 -5
  110. package/src/contracts/index.ts +2 -2
  111. package/src/contracts/multicall.ts +247 -106
  112. package/src/contracts/outbox.ts +42 -8
  113. package/src/contracts/rollup.ts +447 -80
  114. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +91 -30
  115. package/src/deploy_aztec_l1_contracts.ts +32 -18
  116. package/src/foundry_binary.ts +57 -0
  117. package/src/l1_artifacts.ts +12 -35
  118. package/src/l1_contract_addresses.ts +120 -94
  119. package/src/l1_reader.ts +17 -10
  120. package/src/l1_tx_utils/config.ts +3 -3
  121. package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
  122. package/src/l1_tx_utils/l1_tx_utils.ts +122 -48
  123. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +10 -5
  124. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  125. package/src/l1_tx_utils/types.ts +32 -0
  126. package/src/publisher_manager.ts +143 -12
  127. package/src/queries.ts +76 -4
  128. package/src/test/blob_kzg_warmup.ts +65 -0
  129. package/src/test/chain_monitor.ts +148 -3
  130. package/src/test/eth_cheat_codes.ts +82 -45
  131. package/src/test/index.ts +1 -0
  132. package/src/test/rollup_cheat_codes.ts +115 -6
  133. package/src/test/start_anvil.ts +212 -32
  134. package/src/utils.ts +45 -18
  135. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  136. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  137. package/dest/contracts/empire_slashing_proposer.js +0 -216
  138. package/dest/contracts/tally_slashing_proposer.d.ts +0 -140
  139. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  140. package/dest/contracts/tally_slashing_proposer.js +0 -320
  141. package/dest/generated/l1-contracts-defaults.d.ts +0 -30
  142. package/dest/generated/l1-contracts-defaults.d.ts.map +0 -1
  143. package/dest/generated/l1-contracts-defaults.js +0 -30
  144. package/src/contracts/empire_slashing_proposer.ts +0 -265
  145. package/src/generated/l1-contracts-defaults.ts +0 -32
@@ -1,15 +1,15 @@
1
- import type { ConfigMappingsType } from '@aztec/foundation/config';
1
+ import type { ConfigMapping, ConfigMappingsType } from '@aztec/foundation/config';
2
2
  import { EthAddress } from '@aztec/foundation/eth-address';
3
- import { schemas, zodFor } from '@aztec/foundation/schemas';
3
+ import { schemas } from '@aztec/foundation/schemas';
4
4
 
5
5
  import { z } from 'zod';
6
6
 
7
7
  /**
8
- * The names of the current L1 contract addresses.
8
+ * Required L1 contract address keys.
9
9
  * NOTE: When changing this list, make sure to update CLI & CI scripts accordingly.
10
10
  * For reference: https://github.com/AztecProtocol/aztec-packages/pull/5553
11
11
  */
12
- export const L1ContractsNames = [
12
+ const REQUIRED_L1_CONTRACT_ADDRESS_KEYS = [
13
13
  'rollupAddress',
14
14
  'registryAddress',
15
15
  'inboxAddress',
@@ -23,102 +23,128 @@ export const L1ContractsNames = [
23
23
  'stakingAssetAddress',
24
24
  ] as const;
25
25
 
26
+ /** Optional L1 contract address keys — present only in some deployments. */
27
+ export const OPTIONAL_L1_CONTRACT_ADDRESS_KEYS = [
28
+ 'feeAssetHandlerAddress',
29
+ 'stakingAssetHandlerAddress',
30
+ 'zkPassportVerifierAddress',
31
+ 'gseAddress',
32
+ 'dateGatedRelayerAddress',
33
+ ] as const;
34
+
35
+ type RequiredContractKey = (typeof REQUIRED_L1_CONTRACT_ADDRESS_KEYS)[number];
36
+ type OptionalContractKey = (typeof OPTIONAL_L1_CONTRACT_ADDRESS_KEYS)[number];
37
+
26
38
  /** Provides the directory of current L1 contract addresses */
27
39
  export type L1ContractAddresses = {
28
- [K in (typeof L1ContractsNames)[number]]: EthAddress;
40
+ [K in RequiredContractKey]: EthAddress;
29
41
  } & {
30
- slashFactoryAddress?: EthAddress | undefined;
31
- feeAssetHandlerAddress?: EthAddress | undefined;
32
- stakingAssetHandlerAddress?: EthAddress | undefined;
33
- zkPassportVerifierAddress?: EthAddress | undefined;
34
- gseAddress?: EthAddress | undefined;
35
- dateGatedRelayerAddress?: EthAddress | undefined;
42
+ [K in OptionalContractKey]?: EthAddress;
36
43
  };
37
44
 
38
- export const L1ContractAddressesSchema = zodFor<L1ContractAddresses>()(
39
- z.object({
40
- rollupAddress: schemas.EthAddress,
41
- registryAddress: schemas.EthAddress,
42
- inboxAddress: schemas.EthAddress,
43
- outboxAddress: schemas.EthAddress,
44
- feeJuiceAddress: schemas.EthAddress,
45
- stakingAssetAddress: schemas.EthAddress,
46
- feeJuicePortalAddress: schemas.EthAddress,
47
- coinIssuerAddress: schemas.EthAddress,
48
- rewardDistributorAddress: schemas.EthAddress,
49
- governanceProposerAddress: schemas.EthAddress,
50
- governanceAddress: schemas.EthAddress,
51
- slashFactoryAddress: schemas.EthAddress.optional(),
52
- feeAssetHandlerAddress: schemas.EthAddress.optional(),
53
- stakingAssetHandlerAddress: schemas.EthAddress.optional(),
54
- zkPassportVerifierAddress: schemas.EthAddress.optional(),
55
- gseAddress: schemas.EthAddress.optional(),
56
- dateGatedRelayerAddress: schemas.EthAddress.optional(),
57
- }),
58
- );
45
+ /**
46
+ * Names of required L1 contract addresses.
47
+ * NOTE: When changing the set of contracts, update CLI & CI scripts accordingly.
48
+ * For reference: https://github.com/AztecProtocol/aztec-packages/pull/5553
49
+ */
50
+ export const L1ContractsNames = REQUIRED_L1_CONTRACT_ADDRESS_KEYS;
59
51
 
60
52
  const parseEnv = (val: string) => EthAddress.fromString(val);
53
+ const addrEnv = (description: string, env?: ConfigMapping<EthAddress>['env']): ConfigMapping<EthAddress> => ({
54
+ description,
55
+ parseEnv,
56
+ ...(env && { env }),
57
+ });
61
58
 
62
- export const l1ContractAddressesMapping: ConfigMappingsType<
63
- Omit<L1ContractAddresses, 'gseAddress' | 'zkPassportVerifierAddress' | 'dateGatedRelayerAddress'>
64
- > = {
65
- registryAddress: {
66
- env: 'REGISTRY_CONTRACT_ADDRESS',
67
- description: 'The deployed L1 registry contract address.',
68
- parseEnv,
69
- },
70
- slashFactoryAddress: {
71
- env: 'SLASH_FACTORY_CONTRACT_ADDRESS',
72
- description: 'The deployed L1 slashFactory contract address',
73
- parseEnv,
74
- },
75
- feeAssetHandlerAddress: {
76
- env: 'FEE_ASSET_HANDLER_CONTRACT_ADDRESS',
77
- description: 'The deployed L1 feeAssetHandler contract address',
78
- parseEnv,
79
- },
80
- rollupAddress: {
81
- description: 'The deployed L1 rollup contract address.',
82
- parseEnv,
83
- },
84
- inboxAddress: {
85
- description: 'The deployed L1 inbox contract address.',
86
- parseEnv,
87
- },
88
- outboxAddress: {
89
- description: 'The deployed L1 outbox contract address.',
90
- parseEnv,
91
- },
92
- feeJuiceAddress: {
93
- description: 'The deployed L1 Fee Juice contract address.',
94
- parseEnv,
95
- },
96
- stakingAssetAddress: {
97
- description: 'The deployed L1 staking asset contract address.',
98
- parseEnv,
99
- },
100
- feeJuicePortalAddress: {
101
- description: 'The deployed L1 Fee Juice portal contract address.',
102
- parseEnv,
103
- },
104
- coinIssuerAddress: {
105
- description: 'The deployed L1 coinIssuer contract address',
106
- parseEnv,
107
- },
108
- rewardDistributorAddress: {
109
- description: 'The deployed L1 rewardDistributor contract address',
110
- parseEnv,
111
- },
112
- governanceProposerAddress: {
113
- description: 'The deployed L1 governanceProposer contract address',
114
- parseEnv,
115
- },
116
- governanceAddress: {
117
- description: 'The deployed L1 governance contract address',
118
- parseEnv,
119
- },
120
- stakingAssetHandlerAddress: {
121
- description: 'The deployed L1 stakingAssetHandler contract address',
122
- parseEnv,
123
- },
59
+ /**
60
+ * Config mapping for all L1 contract addresses.
61
+ * This is the single source of truth for which contract addresses exist and how to parse them.
62
+ */
63
+ export const l1ContractAddressesMapping: ConfigMappingsType<L1ContractAddresses> = {
64
+ rollupAddress: addrEnv('The deployed L1 rollup contract address.'),
65
+ registryAddress: addrEnv('The deployed L1 registry contract address.', 'REGISTRY_CONTRACT_ADDRESS'),
66
+ inboxAddress: addrEnv('The deployed L1 inbox contract address.'),
67
+ outboxAddress: addrEnv('The deployed L1 outbox contract address.'),
68
+ feeJuiceAddress: addrEnv('The deployed L1 Fee Juice contract address.'),
69
+ feeJuicePortalAddress: addrEnv('The deployed L1 Fee Juice portal contract address.'),
70
+ coinIssuerAddress: addrEnv('The deployed L1 coinIssuer contract address'),
71
+ rewardDistributorAddress: addrEnv('The deployed L1 rewardDistributor contract address'),
72
+ governanceProposerAddress: addrEnv('The deployed L1 governanceProposer contract address'),
73
+ governanceAddress: addrEnv('The deployed L1 governance contract address'),
74
+ stakingAssetAddress: addrEnv('The deployed L1 staking asset contract address.'),
75
+ feeAssetHandlerAddress: addrEnv(
76
+ 'The deployed L1 feeAssetHandler contract address',
77
+ 'FEE_ASSET_HANDLER_CONTRACT_ADDRESS',
78
+ ),
79
+ stakingAssetHandlerAddress: addrEnv('The deployed L1 stakingAssetHandler contract address'),
80
+ zkPassportVerifierAddress: addrEnv('The deployed L1 ZK passport verifier contract address'),
81
+ gseAddress: addrEnv('The deployed L1 GSE contract address'),
82
+ dateGatedRelayerAddress: addrEnv('The deployed L1 date-gated relayer contract address'),
124
83
  };
84
+
85
+ /** Keys present in {@link l1ContractAddressesMapping}. */
86
+ export type L1ContractAddressMappingKey = keyof typeof l1ContractAddressesMapping;
87
+
88
+ type RequiredAddressShape = { [K in RequiredContractKey]: typeof schemas.EthAddress };
89
+ type OptionalAddressShape = { [K in OptionalContractKey]: ReturnType<typeof schemas.EthAddress.optional> };
90
+
91
+ export const L1ContractAddressesSchema = z.object({
92
+ ...(Object.fromEntries(REQUIRED_L1_CONTRACT_ADDRESS_KEYS.map(k => [k, schemas.EthAddress])) as RequiredAddressShape),
93
+ ...(Object.fromEntries(
94
+ OPTIONAL_L1_CONTRACT_ADDRESS_KEYS.map(k => [k, schemas.EthAddress.optional()]),
95
+ ) as OptionalAddressShape),
96
+ });
97
+
98
+ /**
99
+ * Selects entries from {@link l1ContractAddressesMapping} so composed configs can reuse the same
100
+ * env vars, descriptions, and parsers without duplicating definitions.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const mappings = {
105
+ * ...pickL1ContractAddressMappings('rollupAddress'),
106
+ * nodeId: { ... },
107
+ * };
108
+ * ```
109
+ */
110
+ export function pickL1ContractAddressMappings<const K extends readonly L1ContractAddressMappingKey[]>(
111
+ ...keys: K
112
+ ): Pick<typeof l1ContractAddressesMapping, K[number]> {
113
+ return Object.fromEntries(keys.map(key => [key, l1ContractAddressesMapping[key]] as const)) as Pick<
114
+ typeof l1ContractAddressesMapping,
115
+ K[number]
116
+ >;
117
+ }
118
+
119
+ /**
120
+ * Like {@link pickL1ContractAddressMappings}, but returns a fragment of {@link L1ContractAddressesSchema}'s shape for
121
+ * spreading into `z.object({ ...pickL1ContractAddressesSchemaShape('rollupAddress'), localField: z... })`.
122
+ */
123
+ export function pickL1ContractAddressesSchema<const K extends readonly L1ContractAddressMappingKey[]>(
124
+ ...keys: K
125
+ ): Pick<typeof L1ContractAddressesSchema.shape, K[number]> {
126
+ return Object.fromEntries(keys.map(key => [key, L1ContractAddressesSchema.shape[key]])) as Pick<
127
+ typeof L1ContractAddressesSchema.shape,
128
+ K[number]
129
+ >;
130
+ }
131
+
132
+ /** Builds an {@link L1ContractAddresses} object from config that stores those fields at the top level. */
133
+ export function pickL1ContractAddresses<T extends L1ContractAddresses>(config: T): L1ContractAddresses {
134
+ const result: Partial<L1ContractAddresses> = {};
135
+ for (const key of REQUIRED_L1_CONTRACT_ADDRESS_KEYS) {
136
+ result[key] = config[key];
137
+ }
138
+ for (const key of OPTIONAL_L1_CONTRACT_ADDRESS_KEYS) {
139
+ const v = config[key];
140
+ if (v !== undefined) {
141
+ result[key] = v;
142
+ }
143
+ }
144
+ return result as L1ContractAddresses;
145
+ }
146
+
147
+ export function randomL1ContractAddresses(includeOptional: boolean = false): L1ContractAddresses {
148
+ const keys = includeOptional ? [...L1ContractsNames, ...OPTIONAL_L1_CONTRACT_ADDRESS_KEYS] : L1ContractsNames;
149
+ return Object.fromEntries(keys.map(name => [name, EthAddress.random()])) as L1ContractAddresses;
150
+ }
package/src/l1_reader.ts CHANGED
@@ -1,29 +1,30 @@
1
- import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config';
1
+ import {
2
+ type ConfigMappingsType,
3
+ getConfigFromMappings,
4
+ numberConfigHelper,
5
+ optionalNumberConfigHelper,
6
+ } from '@aztec/foundation/config';
2
7
 
3
8
  import { type L1ContractAddresses, l1ContractAddressesMapping } from './l1_contract_addresses.js';
4
9
 
5
10
  /** Configuration of the L1GlobalReader. */
6
- export interface L1ReaderConfig {
11
+ export type L1ReaderConfig = {
7
12
  /** List of URLs of Ethereum RPC nodes that services will connect to (comma separated). */
8
13
  l1RpcUrls: string[];
9
14
  /** The RPC Url of the ethereum debug host for trace and debug methods. */
10
15
  l1DebugRpcUrls: string[];
11
16
  /** The chain ID of the ethereum host. */
12
17
  l1ChainId: number;
13
- /** The deployed l1 contract addresses */
14
- l1Contracts: L1ContractAddresses;
15
18
  /** The polling interval viem uses in ms */
16
19
  viemPollingIntervalMS: number;
17
- }
20
+ /** Timeout for HTTP requests to the L1 RPC node in ms. */
21
+ l1HttpTimeoutMS?: number;
22
+ } & L1ContractAddresses;
18
23
 
19
24
  export const l1ReaderConfigMappings: ConfigMappingsType<L1ReaderConfig> = {
20
- l1Contracts: {
21
- description: 'The deployed L1 contract addresses',
22
- nested: l1ContractAddressesMapping,
23
- },
24
25
  l1ChainId: {
25
26
  env: 'L1_CHAIN_ID',
26
- parseEnv: (val: string) => +val,
27
+ ...optionalNumberConfigHelper(),
27
28
  description: 'The chain ID of the ethereum host.',
28
29
  },
29
30
  l1RpcUrls: {
@@ -43,6 +44,12 @@ export const l1ReaderConfigMappings: ConfigMappingsType<L1ReaderConfig> = {
43
44
  description: 'The polling interval viem uses in ms',
44
45
  ...numberConfigHelper(1_000),
45
46
  },
47
+ l1HttpTimeoutMS: {
48
+ env: 'ETHEREUM_HTTP_TIMEOUT_MS',
49
+ description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
50
+ ...optionalNumberConfigHelper(),
51
+ },
52
+ ...l1ContractAddressesMapping,
46
53
  };
47
54
 
48
55
  export function getL1ReaderConfigFromEnv(): L1ReaderConfig {
@@ -73,7 +73,7 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
73
73
  gasLimitBufferPercentage: {
74
74
  description: 'How much to increase calculated gas limit by (percentage)',
75
75
  env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE',
76
- ...numberConfigHelper(20),
76
+ ...floatConfigHelper(20),
77
77
  },
78
78
  maxGwei: {
79
79
  description: 'Maximum gas price in gwei to be used for transactions.',
@@ -90,12 +90,12 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
90
90
  priorityFeeBumpPercentage: {
91
91
  description: 'How much to increase priority fee by each attempt (percentage)',
92
92
  env: 'L1_PRIORITY_FEE_BUMP_PERCENTAGE',
93
- ...numberConfigHelper(20),
93
+ ...floatConfigHelper(20),
94
94
  },
95
95
  priorityFeeRetryBumpPercentage: {
96
96
  description: 'How much to increase priority fee by each retry attempt (percentage)',
97
97
  env: 'L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE',
98
- ...numberConfigHelper(50),
98
+ ...floatConfigHelper(50),
99
99
  },
100
100
  minimumPriorityFeePerGas: {
101
101
  description:
@@ -791,9 +791,6 @@ export class L1FeeAnalyzer {
791
791
  });
792
792
  }
793
793
 
794
- /**
795
- * Gets the minimum value from an array of bigints
796
- */
797
794
  private minBigInt(values: bigint[]): bigint {
798
795
  if (values.length === 0) {
799
796
  return 0n;
@@ -801,3 +798,78 @@ export class L1FeeAnalyzer {
801
798
  return values.reduce((min, val) => (val < min ? val : min), values[0]);
802
799
  }
803
800
  }
801
+
802
+ /** Per-block fee data for one mined L1 block, used to diagnose whether a tx was underpriced for it. */
803
+ export interface WindowBlockFees {
804
+ blockNumber: bigint;
805
+ timestamp: bigint;
806
+ baseFeePerGas: bigint;
807
+ /** 75th percentile priority fee among the block's txs (gas-weighted, from eth_feeHistory). */
808
+ p75PriorityFee: bigint;
809
+ /** Minimum priority fee among all included txs — the inclusion bar for that block. */
810
+ minIncludedPriorityFee: bigint;
811
+ blockBlobsFull: boolean;
812
+ includedBlobCount: number;
813
+ }
814
+
815
+ /** Safety bound on how far back the window scan walks if the chain head is far ahead of the window. */
816
+ const MAX_WINDOW_SCAN_BLOCKS = 64;
817
+
818
+ /**
819
+ * Reads the already-mined L1 blocks whose timestamps fall in [windowStartS, windowEndS) — the L1
820
+ * inclusion window of an L2 slot — and extracts per-block fee data. Walks block headers to map the
821
+ * window to block numbers, then reads the priority-fee stats from a single eth_feeHistory call
822
+ * (percentile 0 is the cheapest included tx, i.e. the inclusion bar), so no transaction bodies are
823
+ * ever downloaded. Historical reads only, so it never waits on a future block. Returns the blocks in
824
+ * chronological order, [] on error, or the subset mined so far if the window is still in progress.
825
+ * Never throws.
826
+ */
827
+ export async function captureWindowBlockFees(
828
+ client: ViemClient,
829
+ windowStartS: bigint,
830
+ windowEndS: bigint,
831
+ ): Promise<WindowBlockFees[]> {
832
+ try {
833
+ let current = await client.getBlock({ blockTag: 'latest' });
834
+ const inWindow: (typeof current)[] = [];
835
+ let scanned = 0;
836
+ // Walk back from the head, collecting blocks inside the window and skipping any past its end,
837
+ // until we cross below the window start (or hit the safety cap / genesis).
838
+ while (current.timestamp >= windowStartS && scanned < MAX_WINDOW_SCAN_BLOCKS) {
839
+ if (current.timestamp < windowEndS) {
840
+ inWindow.push(current);
841
+ }
842
+ if (current.number === 0n) {
843
+ break;
844
+ }
845
+ current = await client.getBlock({ blockNumber: current.number - 1n });
846
+ scanned++;
847
+ }
848
+ if (inWindow.length === 0) {
849
+ return [];
850
+ }
851
+ inWindow.reverse();
852
+ const newest = inWindow[inWindow.length - 1];
853
+ const feeHistory = await client.getFeeHistory({
854
+ blockCount: Number(newest.number - inWindow[0].number) + 1,
855
+ blockNumber: newest.number,
856
+ rewardPercentiles: [0, 75],
857
+ });
858
+ return inWindow.map(block => {
859
+ const idx = Number(block.number - feeHistory.oldestBlock);
860
+ const [minIncludedPriorityFee, p75PriorityFee] = feeHistory.reward?.[idx] ?? [0n, 0n];
861
+ const blobsInBlock = block.blobGasUsed ? Number(block.blobGasUsed / GAS_PER_BLOB) : 0;
862
+ return {
863
+ blockNumber: block.number,
864
+ timestamp: block.timestamp,
865
+ baseFeePerGas: block.baseFeePerGas ?? 0n,
866
+ p75PriorityFee,
867
+ minIncludedPriorityFee,
868
+ blockBlobsFull: blobsInBlock >= getMaxBlobCapacity(block.timestamp),
869
+ includedBlobCount: blobsInBlock,
870
+ };
871
+ });
872
+ } catch {
873
+ return [];
874
+ }
875
+ }
@@ -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 { retryUntil } from '@aztec/foundation/retry';
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
- if (this.interrupted) {
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
- const baseState = { request, gasLimit, blobInputs, gasPrice, nonce };
263
- const txData = this.makeTxData(baseState, { isCancelTx: false });
278
+ let txHash: Hex;
279
+ let nonce: number;
280
+ let baseState: Pick<L1TxState, 'request' | 'gasLimit' | 'blobInputs' | 'gasPrice' | 'nonce'>;
264
281
 
265
- // Send the new tx
266
- const signedRequest = await this.prepareSignedTransaction(txData);
267
- const txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
268
- // Update after tx is sent successfully
269
- this.lastSentNonce = nonce;
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), 0),
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: number;
441
+ let l1Timestamp = 0;
412
442
 
413
443
  while (true) {
414
- l1Timestamp = await this.getL1Timestamp();
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
- const receipt = await this.monitorTransaction(state);
635
- return { receipt, state };
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
- // Check if the original tx is still pending
687
- const currentNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
688
- if (currentNonce < nonce) {
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`,