@aztec-labs/ethereum 6.0.0-rc.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/blob_tx.d.ts +24 -0
- package/dest/blob_tx.d.ts.map +1 -0
- package/dest/blob_tx.js +94 -0
- package/dest/bn254/hash_to_point.d.ts +32 -0
- package/dest/bn254/hash_to_point.d.ts.map +1 -0
- package/dest/bn254/hash_to_point.js +80 -0
- package/dest/bn254/index.d.ts +3 -0
- package/dest/bn254/index.d.ts.map +1 -0
- package/dest/bn254/index.js +2 -0
- package/dest/bn254/proof_of_possession_gas.d.ts +29 -0
- package/dest/bn254/proof_of_possession_gas.d.ts.map +1 -0
- package/dest/bn254/proof_of_possession_gas.js +24 -0
- package/dest/client.d.ts +1 -1
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +5 -3
- package/dest/contracts/errors.d.ts +10 -3
- package/dest/contracts/errors.d.ts.map +1 -1
- package/dest/contracts/errors.js +10 -4
- package/dest/contracts/inbox.d.ts +1 -1
- package/dest/contracts/inbox.d.ts.map +1 -1
- package/dest/contracts/inbox.js +2 -0
- package/dest/contracts/log.d.ts +7 -1
- package/dest/contracts/log.d.ts.map +1 -1
- package/dest/contracts/log.js +6 -0
- package/dest/contracts/rollup.d.ts +32 -4
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +63 -7
- package/dest/deploy_aztec_l1_contracts.d.ts +16 -1
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +201 -65
- package/dest/foundry_binary.d.ts +1 -1
- package/dest/foundry_binary.d.ts.map +1 -1
- package/dest/foundry_binary.js +2 -1
- package/dest/l1_tx_utils/amsterdam.d.ts +1 -1
- package/dest/l1_tx_utils/amsterdam.d.ts.map +1 -1
- package/dest/l1_tx_utils/amsterdam.js +4 -1
- package/dest/l1_tx_utils/constants.d.ts +2 -1
- package/dest/l1_tx_utils/constants.d.ts.map +1 -1
- package/dest/l1_tx_utils/constants.js +3 -0
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts +1 -1
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts.map +1 -1
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +6 -1
- package/dest/l1_tx_utils/index.d.ts +2 -1
- package/dest/l1_tx_utils/index.d.ts.map +1 -1
- package/dest/l1_tx_utils/index.js +1 -0
- package/dest/l1_tx_utils/l1_tx_utils.d.ts +21 -1
- package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_tx_utils.js +71 -5
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +8 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.js +1 -1
- package/dest/l1_tx_utils/send_cost.d.ts +15 -0
- package/dest/l1_tx_utils/send_cost.d.ts.map +1 -0
- package/dest/l1_tx_utils/send_cost.js +10 -0
- package/dest/l1_tx_utils/signer.d.ts +2 -2
- package/dest/l1_tx_utils/signer.d.ts.map +1 -1
- package/dest/l1_tx_utils/signer.js +2 -2
- package/dest/l1_tx_utils/tx_delayer.d.ts +1 -1
- package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
- package/dest/l1_tx_utils/tx_delayer.js +8 -26
- package/dest/publisher_manager.d.ts +127 -13
- package/dest/publisher_manager.d.ts.map +1 -1
- package/dest/publisher_manager.js +320 -79
- package/dest/test/anvil_hardfork.d.ts +42 -0
- package/dest/test/anvil_hardfork.d.ts.map +1 -0
- package/dest/test/anvil_hardfork.js +107 -0
- package/dest/test/eth_cheat_codes.d.ts +30 -5
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +283 -101
- package/dest/test/index.d.ts +3 -1
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +2 -0
- package/dest/test/l1_hardfork.d.ts +43 -0
- package/dest/test/l1_hardfork.d.ts.map +1 -0
- package/dest/test/l1_hardfork.js +51 -0
- package/dest/test/recording_rpc_server.d.ts +1 -1
- package/dest/test/recording_rpc_server.d.ts.map +1 -1
- package/dest/test/recording_rpc_server.js +1 -0
- package/dest/test/rollup_cheat_codes.d.ts +1 -1
- package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
- package/dest/test/rollup_cheat_codes.js +7 -7
- package/dest/test/start_anvil.d.ts +14 -2
- package/dest/test/start_anvil.d.ts.map +1 -1
- package/dest/test/start_anvil.js +28 -11
- package/package.json +13 -11
- package/src/blob_tx.ts +118 -0
- package/src/bn254/hash_to_point.ts +95 -0
- package/src/bn254/index.ts +2 -0
- package/src/bn254/proof_of_possession_gas.ts +54 -0
- package/src/client.ts +6 -3
- package/src/contracts/errors.ts +12 -4
- package/src/contracts/inbox.ts +4 -0
- package/src/contracts/log.ts +12 -0
- package/src/contracts/rollup.ts +63 -6
- package/src/deploy_aztec_l1_contracts.ts +238 -63
- package/src/foundry_binary.ts +2 -1
- package/src/l1_tx_utils/amsterdam.ts +4 -1
- package/src/l1_tx_utils/constants.ts +4 -0
- package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +11 -1
- package/src/l1_tx_utils/index.ts +1 -0
- package/src/l1_tx_utils/l1_tx_utils.ts +72 -5
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +1 -1
- package/src/l1_tx_utils/send_cost.ts +21 -0
- package/src/l1_tx_utils/signer.ts +3 -2
- package/src/l1_tx_utils/tx_delayer.ts +8 -37
- package/src/publisher_manager.ts +458 -83
- package/src/test/anvil_hardfork.ts +95 -0
- package/src/test/eth_cheat_codes.ts +306 -81
- package/src/test/fixtures/sepolia_blob_tx.json +10 -0
- package/src/test/index.ts +2 -0
- package/src/test/l1_hardfork.ts +71 -0
- package/src/test/recording_rpc_server.ts +1 -0
- package/src/test/rollup_cheat_codes.ts +9 -9
- package/src/test/start_anvil.ts +41 -13
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { SlotNumber } from '@aztec-labs/foundation/branded-types';
|
|
2
2
|
import { SecretValue, getActiveNetworkName } from '@aztec-labs/foundation/config';
|
|
3
|
+
import { getEnv } from '@aztec-labs/foundation/config/env';
|
|
3
4
|
import { EthAddress } from '@aztec-labs/foundation/eth-address';
|
|
4
5
|
import { jsonStringify } from '@aztec-labs/foundation/json-rpc';
|
|
5
6
|
import { createLogger } from '@aztec-labs/foundation/log';
|
|
6
7
|
import { promiseWithResolvers } from '@aztec-labs/foundation/promise';
|
|
7
8
|
import type { Fr } from '@aztec-labs/foundation/schemas';
|
|
9
|
+
import { parse as parseToml } from '@iarna/toml';
|
|
8
10
|
import { bn254 } from '@noble/curves/bn254';
|
|
9
11
|
import type { Abi, Narrow } from 'abitype';
|
|
10
|
-
import { spawn } from 'child_process';
|
|
11
|
-
import {
|
|
12
|
+
import { execFileSync, spawn } from 'child_process';
|
|
13
|
+
import {
|
|
14
|
+
cpSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
mkdtempSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
realpathSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
statSync,
|
|
22
|
+
utimesSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'fs';
|
|
12
25
|
import { createRequire } from 'node:module';
|
|
13
26
|
import { tmpdir } from 'os';
|
|
14
|
-
import { basename, dirname, join, resolve } from 'path';
|
|
27
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'path';
|
|
15
28
|
import readline from 'readline';
|
|
16
29
|
import type { Hex } from 'viem';
|
|
17
30
|
import { mainnet, sepolia } from 'viem/chains';
|
|
@@ -32,27 +45,96 @@ const require = createRequire(import.meta.url);
|
|
|
32
45
|
|
|
33
46
|
const JSON_DEPLOY_RESULT_PREFIX = 'JSON DEPLOY RESULT:';
|
|
34
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Extends a broadcast deadline for a `--slow` broadcast, which waits for each transaction's receipt before sending the
|
|
50
|
+
* next, so on a chain that mines on an interval it needs at least one block per transaction.
|
|
51
|
+
*/
|
|
52
|
+
export function getSlowBroadcastTimeout(timeout: number | undefined, txCount: number): number | undefined {
|
|
53
|
+
return timeout === undefined ? undefined : timeout + txCount * SLOW_BROADCAST_ALLOWANCE_PER_TX_MS;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One mainnet slot per transaction, which also covers the slowest anvil block intervals the tests use. */
|
|
57
|
+
const SLOW_BROADCAST_ALLOWANCE_PER_TX_MS = 12_000;
|
|
58
|
+
|
|
59
|
+
/** Returns the broadcast deadline, with an optional environment override in milliseconds. */
|
|
60
|
+
export function getForgeBroadcastTimeout(
|
|
61
|
+
chainId: number,
|
|
62
|
+
value = getEnv().FORGE_BROADCAST_TIMEOUT_MS,
|
|
63
|
+
): number | undefined {
|
|
64
|
+
if (value === undefined || value === '') {
|
|
65
|
+
return isAnvilTestChain(chainId) ? 120_000 : undefined;
|
|
66
|
+
}
|
|
67
|
+
const timeout = Number(value);
|
|
68
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 2_147_483_647) {
|
|
69
|
+
throw new Error('FORGE_BROADCAST_TIMEOUT_MS must be an integer between 0 and 2147483647');
|
|
70
|
+
}
|
|
71
|
+
return timeout === 0 ? undefined : timeout;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Returns Forge broadcast flags for the target chain and artifact EVM version. */
|
|
75
|
+
export function getForgeBroadcastArgs(l1ContractsPath: string, chainId: number): string[] {
|
|
76
|
+
const broadcastArgs = ['--broadcast', '--disable-external-identification'];
|
|
77
|
+
if (chainId !== sepolia.id) {
|
|
78
|
+
return broadcastArgs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const foundryToml = readFileSync(join(l1ContractsPath, 'foundry.toml'), 'utf-8');
|
|
82
|
+
const profile = parseToml(foundryToml).profile;
|
|
83
|
+
const defaultProfile = typeof profile === 'object' && 'default' in profile ? profile.default : undefined;
|
|
84
|
+
const evmVersion =
|
|
85
|
+
defaultProfile && typeof defaultProfile === 'object' && 'evm_version' in defaultProfile
|
|
86
|
+
? defaultProfile.evm_version
|
|
87
|
+
: undefined;
|
|
88
|
+
if (typeof evmVersion !== 'string' || !evmVersion) {
|
|
89
|
+
throw new Error('L1 deployment artifacts do not specify an EVM version');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Plan with the artifact's EVM target; --skip-simulation makes Forge estimate each send through RPC.
|
|
93
|
+
return [...broadcastArgs, '--hardfork', evmVersion, '--skip-simulation'];
|
|
94
|
+
}
|
|
95
|
+
|
|
35
96
|
/**
|
|
36
97
|
* Runs a process and parses JSON deploy results from stdout.
|
|
37
98
|
* Lines starting with JSON_DEPLOY_RESULT_PREFIX are parsed and returned.
|
|
38
99
|
* All other stdout goes to logger.info, stderr goes to logger.warn.
|
|
39
100
|
*/
|
|
40
|
-
function runProcess<T>(
|
|
101
|
+
export function runProcess<T>(
|
|
41
102
|
command: string,
|
|
42
103
|
args: string[],
|
|
43
104
|
env: Record<string, string | undefined>,
|
|
44
105
|
cwd: string,
|
|
106
|
+
timeoutMs?: number,
|
|
45
107
|
): Promise<T | undefined> {
|
|
46
108
|
const { promise, resolve, reject } = promiseWithResolvers<T | undefined>();
|
|
47
109
|
const proc = spawn(command, args, {
|
|
48
110
|
cwd,
|
|
49
|
-
env: { ...
|
|
111
|
+
env: { ...getEnv(), ...env },
|
|
50
112
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
113
|
});
|
|
52
114
|
|
|
53
115
|
let result: T | undefined;
|
|
54
116
|
let parseError: Error | undefined;
|
|
55
117
|
let settled = false;
|
|
118
|
+
let timedOut = false;
|
|
119
|
+
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
120
|
+
const timeoutTimer = timeoutMs
|
|
121
|
+
? setTimeout(() => {
|
|
122
|
+
timedOut = true;
|
|
123
|
+
proc.kill('SIGTERM');
|
|
124
|
+
killTimer = setTimeout(() => proc.kill('SIGKILL'), 1000);
|
|
125
|
+
}, timeoutMs)
|
|
126
|
+
: undefined;
|
|
127
|
+
|
|
128
|
+
const clearTimers = () => {
|
|
129
|
+
clearTimeout(timeoutTimer);
|
|
130
|
+
clearTimeout(killTimer);
|
|
131
|
+
};
|
|
132
|
+
const rejectProcess = (error: Error) => {
|
|
133
|
+
if (preparedDeployDirs.delete(cwd)) {
|
|
134
|
+
error.message += `; deployment artifacts preserved at ${cwd}. Transactions may already have been broadcast.`;
|
|
135
|
+
}
|
|
136
|
+
reject(error);
|
|
137
|
+
};
|
|
56
138
|
|
|
57
139
|
readline.createInterface({ input: proc.stdout }).on('line', line => {
|
|
58
140
|
const trimmedLine = line.trim();
|
|
@@ -74,18 +156,24 @@ function runProcess<T>(
|
|
|
74
156
|
return;
|
|
75
157
|
}
|
|
76
158
|
settled = true;
|
|
77
|
-
|
|
159
|
+
clearTimers();
|
|
160
|
+
rejectProcess(new Error(`Failed to spawn ${command}: ${error.message}`));
|
|
78
161
|
});
|
|
79
162
|
|
|
80
|
-
proc.on('close', code => {
|
|
163
|
+
proc.on('close', (code, signal) => {
|
|
81
164
|
if (settled) {
|
|
82
165
|
return;
|
|
83
166
|
}
|
|
84
167
|
settled = true;
|
|
85
|
-
|
|
86
|
-
|
|
168
|
+
clearTimers();
|
|
169
|
+
if (timedOut) {
|
|
170
|
+
rejectProcess(new Error(`${command} timed out after ${timeoutMs}ms (signal ${signal ?? 'none'})`));
|
|
171
|
+
} else if (signal) {
|
|
172
|
+
rejectProcess(new Error(`${command} terminated by signal ${signal}`));
|
|
173
|
+
} else if (code !== 0) {
|
|
174
|
+
rejectProcess(new Error(`${command} exited with code ${code}`));
|
|
87
175
|
} else if (parseError) {
|
|
88
|
-
|
|
176
|
+
rejectProcess(parseError);
|
|
89
177
|
} else {
|
|
90
178
|
resolve(result);
|
|
91
179
|
}
|
|
@@ -129,52 +217,77 @@ async function runForgeScriptBroadcast<T extends { rollupAddress: Hex }>(
|
|
|
129
217
|
const { forgeBin, l1ContractsPath, script, privateKey, rpcUrl, l1Client, env } = args;
|
|
130
218
|
const baseArgs = ['script', script, '--sig', 'run()', '--private-key', privateKey, '--rpc-url', rpcUrl];
|
|
131
219
|
const verifyArgs = args.verify ? ['--verify'] : [];
|
|
220
|
+
const chainId = await l1Client.getChainId();
|
|
221
|
+
const broadcastArgs = getForgeBroadcastArgs(l1ContractsPath, chainId);
|
|
222
|
+
const timeout = getForgeBroadcastTimeout(chainId);
|
|
132
223
|
|
|
133
224
|
const latestBlock = await l1Client.request({ method: 'eth_getBlockByNumber', params: ['latest', false] });
|
|
225
|
+
let result: T | undefined;
|
|
134
226
|
if (!latestBlock || !isAmsterdamBlock(latestBlock)) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
227
|
+
result = await runProcess<T>(
|
|
228
|
+
forgeBin,
|
|
229
|
+
[...baseArgs, ...broadcastArgs, ...verifyArgs],
|
|
230
|
+
env,
|
|
231
|
+
l1ContractsPath,
|
|
232
|
+
timeout,
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
logger.info(`Dry-running ${script} to plan Glamsterdam gas limits`);
|
|
236
|
+
result = await runProcess<T>(
|
|
237
|
+
forgeBin,
|
|
238
|
+
[
|
|
239
|
+
...baseArgs,
|
|
240
|
+
...broadcastArgs.filter(arg => arg !== '--broadcast' && arg !== '--skip-simulation'),
|
|
241
|
+
'--gas-estimate-multiplier',
|
|
242
|
+
String(GLAMSTERDAM_GAS_ESTIMATE_MULTIPLIER),
|
|
243
|
+
],
|
|
244
|
+
env,
|
|
141
245
|
l1ContractsPath,
|
|
142
246
|
);
|
|
143
|
-
}
|
|
144
247
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
248
|
+
// Leave headroom for the block gas limit drifting down before the last tx lands.
|
|
249
|
+
const maxTxGas = (BigInt(latestBlock.gasLimit) * 99n) / 100n;
|
|
250
|
+
const sequencePath = join(
|
|
251
|
+
l1ContractsPath,
|
|
252
|
+
'broadcast',
|
|
253
|
+
basename(script),
|
|
254
|
+
String(chainId),
|
|
255
|
+
'dry-run',
|
|
256
|
+
'run-latest.json',
|
|
257
|
+
);
|
|
258
|
+
const sequence = JSON.parse(readFileSync(sequencePath, 'utf-8'));
|
|
259
|
+
for (const tx of sequence.transactions) {
|
|
260
|
+
const gas = BigInt(tx.transaction.gas);
|
|
261
|
+
tx.transaction.gas = `0x${(gas < maxTxGas ? gas : maxTxGas).toString(16)}`;
|
|
262
|
+
}
|
|
263
|
+
// Forge 1.8 resumes from a recovery snapshot under cache/ (fingerprinted, so it cannot be edited) instead of from the
|
|
264
|
+
// broadcast file, and would send the unclamped limits, some larger than a block. Dropping the snapshot and the
|
|
265
|
+
// generation the exported files point at makes it resume from the edited broadcast file, as older forge versions did.
|
|
266
|
+
delete sequence.recovery_generation;
|
|
267
|
+
writeFileSync(sequencePath, JSON.stringify(sequence, null, 2));
|
|
268
|
+
const cacheDir = join(l1ContractsPath, 'cache', basename(script), String(chainId), 'dry-run');
|
|
269
|
+
const sensitivePath = join(cacheDir, 'run-latest.json');
|
|
270
|
+
if (existsSync(sensitivePath)) {
|
|
271
|
+
const sensitive = JSON.parse(readFileSync(sensitivePath, 'utf-8'));
|
|
272
|
+
delete sensitive.recovery_generation;
|
|
273
|
+
writeFileSync(sensitivePath, JSON.stringify(sensitive, null, 2));
|
|
274
|
+
}
|
|
275
|
+
rmSync(join(cacheDir, 'run-latest.json.recovery.json'), { force: true });
|
|
152
276
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
164
|
-
const sequence = JSON.parse(readFileSync(sequencePath, 'utf-8'));
|
|
165
|
-
for (const tx of sequence.transactions) {
|
|
166
|
-
const gas = BigInt(tx.transaction.gas);
|
|
167
|
-
tx.transaction.gas = `0x${(gas < maxTxGas ? gas : maxTxGas).toString(16)}`;
|
|
277
|
+
logger.info(`Broadcasting ${sequence.transactions.length} transactions from ${script}`, { maxTxGas });
|
|
278
|
+
// --slow waits for each receipt before sending the next tx, so the sender only needs balance for one gas limit at a
|
|
279
|
+
// time and a block-sized tx is not queued behind others from the same sender.
|
|
280
|
+
await runProcess(
|
|
281
|
+
forgeBin,
|
|
282
|
+
[...baseArgs, ...broadcastArgs, '--resume', '--slow', ...verifyArgs],
|
|
283
|
+
env,
|
|
284
|
+
l1ContractsPath,
|
|
285
|
+
getSlowBroadcastTimeout(timeout, sequence.transactions.length),
|
|
286
|
+
);
|
|
168
287
|
}
|
|
169
|
-
writeFileSync(sequencePath, JSON.stringify(sequence, null, 2));
|
|
170
|
-
|
|
171
|
-
logger.info(`Broadcasting ${sequence.transactions.length} transactions from ${script}`, { maxTxGas });
|
|
172
|
-
// --slow waits for each receipt before sending the next tx, so the sender only needs balance for one gas limit at a
|
|
173
|
-
// time and a block-sized tx is not queued behind others from the same sender.
|
|
174
|
-
await runProcess(forgeBin, [...baseArgs, '--resume', '--slow', ...verifyArgs], env, l1ContractsPath);
|
|
175
288
|
|
|
176
289
|
// l1-contracts releases that ship this script no longer activate initial validators inside the deploy, and rely on
|
|
177
|
-
//
|
|
290
|
+
// the broadcast wrapper flushing the entry queue afterwards; we bypassed it, so flush here. Older releases flush inline.
|
|
178
291
|
if (
|
|
179
292
|
result &&
|
|
180
293
|
hasInitialValidators(env.INITIAL_VALIDATORS) &&
|
|
@@ -195,13 +308,12 @@ async function runForgeScriptBroadcast<T extends { rollupAddress: Hex }>(
|
|
|
195
308
|
privateKey,
|
|
196
309
|
'--rpc-url',
|
|
197
310
|
rpcUrl,
|
|
311
|
+
...broadcastArgs.filter(arg => arg !== '--skip-simulation'),
|
|
198
312
|
'--skip-simulation',
|
|
199
|
-
'--batch-size',
|
|
200
|
-
'1',
|
|
201
|
-
'--broadcast',
|
|
202
313
|
],
|
|
203
314
|
env,
|
|
204
315
|
l1ContractsPath,
|
|
316
|
+
timeout,
|
|
205
317
|
);
|
|
206
318
|
}
|
|
207
319
|
return result;
|
|
@@ -258,37 +370,32 @@ export function getL1ContractsPath(): string {
|
|
|
258
370
|
return resolve(dirname(require.resolve('@aztec-foundation/l1-artifacts')), '..', 'l1-contracts');
|
|
259
371
|
}
|
|
260
372
|
|
|
261
|
-
|
|
262
|
-
let preparedDeployDir: string | undefined;
|
|
373
|
+
const preparedDeployDirs = new Set<string>();
|
|
263
374
|
|
|
264
375
|
function cleanupDeployDir() {
|
|
265
|
-
|
|
376
|
+
for (const directory of preparedDeployDirs) {
|
|
266
377
|
try {
|
|
267
|
-
rmSync(
|
|
378
|
+
rmSync(directory, { recursive: true, force: true });
|
|
268
379
|
} catch {
|
|
269
380
|
// ignore cleanup errors
|
|
270
381
|
}
|
|
271
|
-
preparedDeployDir = undefined;
|
|
272
382
|
}
|
|
383
|
+
preparedDeployDirs.clear();
|
|
273
384
|
}
|
|
274
385
|
|
|
386
|
+
process.once('exit', cleanupDeployDir);
|
|
387
|
+
|
|
275
388
|
/**
|
|
276
389
|
* Prepares a temp directory for forge deployment.
|
|
277
390
|
* Copies all artifacts with preserved timestamps (required for forge cache validity).
|
|
278
391
|
* A fresh broadcast/ directory is created for deployment outputs.
|
|
279
392
|
*/
|
|
280
393
|
export function prepareL1ContractsForDeployment(): string {
|
|
281
|
-
if (preparedDeployDir && existsSync(preparedDeployDir)) {
|
|
282
|
-
logger.verbose(`Using cached deployment directory: ${preparedDeployDir}`);
|
|
283
|
-
return preparedDeployDir;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
394
|
const basePath = getL1ContractsPath();
|
|
287
395
|
logger.verbose(`Preparing L1 contracts from: ${basePath}`);
|
|
288
396
|
const tempDir = mkdtempSync(join(tmpdir(), '.foundry-deploy-'));
|
|
289
397
|
logger.verbose(`Created temp directory for deployment: ${tempDir}`);
|
|
290
|
-
|
|
291
|
-
process.on('exit', cleanupDeployDir);
|
|
398
|
+
preparedDeployDirs.add(tempDir);
|
|
292
399
|
|
|
293
400
|
// Copy all dirs with preserved timestamps (required for forge cache validity)
|
|
294
401
|
const copyOpts = { recursive: true, preserveTimestamps: true };
|
|
@@ -315,12 +422,80 @@ export function prepareL1ContractsForDeployment(): string {
|
|
|
315
422
|
foundryToml = foundryToml.replace(/solc\s*=\s*"\.\/solc-[^"]+"/, `solc = "${absoluteSolcPath}"`);
|
|
316
423
|
logger.verbose(`Updated solc path in foundry.toml to: ${absoluteSolcPath}`);
|
|
317
424
|
}
|
|
425
|
+
|
|
426
|
+
const cachePath = join(tempDir, FORGE_CACHE_FILE);
|
|
427
|
+
const cache: ForgeCache = JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
428
|
+
if (!cache.preprocessed) {
|
|
429
|
+
// A cache written without test preprocessing is only accepted by forge 1.8 if dynamic test linking is off. Test
|
|
430
|
+
// preprocessing does not affect the contracts and scripts we deploy.
|
|
431
|
+
foundryToml = foundryToml.replace(/^\[profile\.default\]$/m, '$&\ndynamic_test_linking = false');
|
|
432
|
+
}
|
|
318
433
|
writeFileSync(join(tempDir, 'foundry.toml'), foundryToml);
|
|
319
434
|
|
|
435
|
+
try {
|
|
436
|
+
patchForgeCache(tempDir, cache);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
logger.warn(`Failed to patch the forge cache, forge will recompile the L1 contracts`, { err });
|
|
439
|
+
}
|
|
440
|
+
|
|
320
441
|
mkdirSync(join(tempDir, 'broadcast'));
|
|
321
442
|
return tempDir;
|
|
322
443
|
}
|
|
323
444
|
|
|
445
|
+
const FORGE_CACHE_FILE = join('cache', 'solidity-files-cache.json');
|
|
446
|
+
|
|
447
|
+
/** The fields of forge's `solidity-files-cache.json` that depend on the forge version or the project location. */
|
|
448
|
+
type ForgeCache = {
|
|
449
|
+
profiles?: Record<string, { solc?: Record<string, unknown> }>;
|
|
450
|
+
remappings?: string[];
|
|
451
|
+
preprocessed?: boolean;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Makes the forge build cache shipped in l1-artifacts valid in a deploy directory, so forge uses the shipped artifacts
|
|
456
|
+
* instead of recompiling all L1 contracts, which takes ~10s per process and yields bytecode with a different metadata
|
|
457
|
+
* hash than the published one.
|
|
458
|
+
*
|
|
459
|
+
* Since 1.8, forge stores the absolute remappings of the project in its cache, and discards a cache whose remappings do
|
|
460
|
+
* not match the current project root. So a cache written in the directory where l1-artifacts was built never matches a
|
|
461
|
+
* copy of it. We regenerate the remappings for the deploy directory in the format forge writes them, which lists every
|
|
462
|
+
* context remapping twice: once with a relative context and once with an absolute one.
|
|
463
|
+
*
|
|
464
|
+
* Caches written by forge before 1.8 also lack the `viaSSACFG` and `experimental` solc settings, and need dynamic test
|
|
465
|
+
* linking disabled (see `prepareL1ContractsForDeployment`).
|
|
466
|
+
*
|
|
467
|
+
* TODO: Once l1-artifacts is built with forge 1.8.5 or later, remove the `viaSSACFG`/`experimental` backfill and the
|
|
468
|
+
* `dynamic_test_linking = false` override, as such caches already carry both settings and record `preprocessed: true`.
|
|
469
|
+
* Keep the remappings rewrite: a forge 1.8 cache stays tied to the directory where it was written.
|
|
470
|
+
*/
|
|
471
|
+
function patchForgeCache(deployDir: string, cache: ForgeCache) {
|
|
472
|
+
const root = realpathSync(deployDir);
|
|
473
|
+
const remappings = execFileSync(resolveFoundryBinary('forge'), ['remappings'], { cwd: root, encoding: 'utf-8' })
|
|
474
|
+
.split('\n')
|
|
475
|
+
.filter(line => line.trim())
|
|
476
|
+
.flatMap(line => {
|
|
477
|
+
const separator = line.indexOf('=');
|
|
478
|
+
const context = line.slice(0, separator).trim();
|
|
479
|
+
const target = line.slice(separator + 1).trim();
|
|
480
|
+
const absoluteTarget = isAbsolute(target) ? target : `${root}/${target}`;
|
|
481
|
+
const remapping = `${context}=${absoluteTarget}`;
|
|
482
|
+
return context.includes(':') ? [remapping, `${root}/${remapping}`] : [remapping];
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
for (const profile of Object.values(cache.profiles ?? {})) {
|
|
486
|
+
if (profile.solc) {
|
|
487
|
+
profile.solc.viaSSACFG ??= false;
|
|
488
|
+
profile.solc.experimental ??= false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
cache.remappings = remappings;
|
|
492
|
+
|
|
493
|
+
const cachePath = join(deployDir, FORGE_CACHE_FILE);
|
|
494
|
+
const { atime, mtime } = statSync(cachePath);
|
|
495
|
+
writeFileSync(cachePath, JSON.stringify(cache));
|
|
496
|
+
utimesSync(cachePath, atime, mtime);
|
|
497
|
+
}
|
|
498
|
+
|
|
324
499
|
/**
|
|
325
500
|
* Computes the validator data for passing to Solidity.
|
|
326
501
|
* Only computes the G2 public key (which requires scalar multiplication on G2, not available in EVM).
|
|
@@ -450,9 +625,9 @@ export async function deployAztecL1Contracts(
|
|
|
450
625
|
|
|
451
626
|
// Verify contracts on Etherscan when on mainnet/sepolia and ETHERSCAN_API_KEY is available.
|
|
452
627
|
const isVerifiableChain = chainId === mainnet.id || chainId === sepolia.id;
|
|
453
|
-
const shouldVerify = isVerifiableChain && !!
|
|
628
|
+
const shouldVerify = isVerifiableChain && !!getEnv().ETHERSCAN_API_KEY;
|
|
454
629
|
|
|
455
|
-
if (isVerifiableChain && !
|
|
630
|
+
if (isVerifiableChain && !getEnv().ETHERSCAN_API_KEY) {
|
|
456
631
|
logger.warn(
|
|
457
632
|
`Deploying to chain ${chainId} (${chainId === mainnet.id ? 'mainnet' : 'sepolia'}) without ETHERSCAN_API_KEY. ` +
|
|
458
633
|
`Contracts will NOT be verified on Etherscan. Set ETHERSCAN_API_KEY environment variable to enable verification.`,
|
package/src/foundry_binary.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getEnv } from '@aztec-labs/foundation/config/env';
|
|
1
2
|
import { spawnSync } from 'child_process';
|
|
2
3
|
import { accessSync, constants } from 'fs';
|
|
3
4
|
import { homedir } from 'os';
|
|
@@ -25,7 +26,7 @@ function isExecutable(path: string): boolean {
|
|
|
25
26
|
*/
|
|
26
27
|
export function resolveFoundryBinary(name: string): string {
|
|
27
28
|
const envVar = `${name.toUpperCase()}_BIN`;
|
|
28
|
-
const envBin =
|
|
29
|
+
const envBin = getEnv()[envVar];
|
|
29
30
|
if (envBin) {
|
|
30
31
|
if (!isExecutable(envBin)) {
|
|
31
32
|
throw new Error(`$${envVar} is set to ${envBin}, which does not exist or is not executable.`);
|
|
@@ -37,8 +37,11 @@ export class AmsterdamForkDetector {
|
|
|
37
37
|
|
|
38
38
|
/** Records a freshly fetched latest L1 block. */
|
|
39
39
|
public observe(block: object | null | undefined): void {
|
|
40
|
-
if (isAmsterdamBlock(block)) {
|
|
40
|
+
if (!this.active && isAmsterdamBlock(block)) {
|
|
41
41
|
this.active = true;
|
|
42
|
+
(this.opts.log ?? createLogger('ethereum:amsterdam-fork-detector')).debug('Amsterdam fork detected on L1', {
|
|
43
|
+
blockNumber: block && 'number' in block ? block.number : undefined,
|
|
44
|
+
});
|
|
42
45
|
}
|
|
43
46
|
this.lastObservedAtMs = this.now();
|
|
44
47
|
}
|
|
@@ -18,6 +18,10 @@ export const MAX_L1_TX_LIMIT = 16_777_216n;
|
|
|
18
18
|
export const AMSTERDAM_MAX_L1_TX_LIMIT = 50_000_000n;
|
|
19
19
|
export const AMSTERDAM_MAX_SLASHED_VALIDATORS_PER_ROUND = 35;
|
|
20
20
|
|
|
21
|
+
// How long a publisher whose send was rejected for insufficient funds is skipped by publisher selection if its balance
|
|
22
|
+
// does not increase in the meantime.
|
|
23
|
+
export const INSUFFICIENT_FUNDS_BACKOFF_MS = 5 * 60 * 1000;
|
|
24
|
+
|
|
21
25
|
// setting a minimum bump percentage to 10% due to geth's implementation
|
|
22
26
|
// https://github.com/ethereum/go-ethereum/blob/e3d61e6db028c412f74bc4d4c7e117a9e29d0de0/core/txpool/legacypool/list.go#L298
|
|
23
27
|
export const MIN_REPLACEMENT_BUMP_PERCENTAGE = 10;
|
|
@@ -28,7 +28,7 @@ export const P75AllTxsPriorityFeeStrategy: PriorityFeeStrategy = {
|
|
|
28
28
|
await Promise.allSettled([
|
|
29
29
|
client.getBlock({ blockTag: 'latest' }),
|
|
30
30
|
isBlobTx ? client.getBlobBaseFee() : Promise.resolve(undefined),
|
|
31
|
-
client.estimateMaxPriorityFeePerGas()
|
|
31
|
+
client.estimateMaxPriorityFeePerGas(),
|
|
32
32
|
client.getBlock({ blockTag: 'pending', includeTransactions: true }).catch(() => null),
|
|
33
33
|
client
|
|
34
34
|
.getFeeHistory({
|
|
@@ -140,6 +140,16 @@ export const P75AllTxsPriorityFeeStrategy: PriorityFeeStrategy = {
|
|
|
140
140
|
});
|
|
141
141
|
competitiveFee = maxReasonableFee;
|
|
142
142
|
debugInfo.cappedToGwei = formatGwei(maxReasonableFee);
|
|
143
|
+
} else if (networkEstimateResult.status === 'fulfilled' && networkEstimate === 0n && competitiveFee > 0n) {
|
|
144
|
+
logger?.warn(
|
|
145
|
+
'L1 RPC returned a zero priority fee estimate, disabling the competitive fee sanity cap. ' +
|
|
146
|
+
'You may be overpaying L1 transaction fees. Review your L1 RPC gas price oracle settings; ' +
|
|
147
|
+
'if using Reth, consider setting --gpo.ignoreprice=2 to exclude zero- and one-wei tips.',
|
|
148
|
+
{
|
|
149
|
+
networkEstimateGwei: formatGwei(networkEstimate),
|
|
150
|
+
competitiveFeeGwei: formatGwei(competitiveFee),
|
|
151
|
+
},
|
|
152
|
+
);
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
// Log final decision
|
package/src/l1_tx_utils/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './interfaces.js';
|
|
|
7
7
|
export * from './l1_fee_analyzer.js';
|
|
8
8
|
export * from './l1_tx_utils.js';
|
|
9
9
|
export * from './readonly_l1_tx_utils.js';
|
|
10
|
+
export * from './send_cost.js';
|
|
10
11
|
export * from './signer.js';
|
|
11
12
|
export * from './types.js';
|
|
12
13
|
export * from './tx_delayer.js';
|
|
@@ -10,25 +10,27 @@ import { Semaphore } from '@aztec-labs/foundation/queue';
|
|
|
10
10
|
import { makeBackoff, retry, retryUntil } from '@aztec-labs/foundation/retry';
|
|
11
11
|
import { sleep } from '@aztec-labs/foundation/sleep';
|
|
12
12
|
import { DateProvider } from '@aztec-labs/foundation/timer';
|
|
13
|
+
import { getErrorCause } from '@aztec-labs/foundation/types';
|
|
13
14
|
import pickBy from 'lodash.pickby';
|
|
14
15
|
import {
|
|
15
16
|
type Abi,
|
|
16
17
|
type BlockOverrides,
|
|
17
18
|
type Hex,
|
|
19
|
+
InsufficientFundsError,
|
|
18
20
|
type PrepareTransactionRequestRequest,
|
|
19
21
|
type StateOverride,
|
|
20
22
|
type TransactionReceipt,
|
|
21
23
|
type TransactionSerializable,
|
|
22
24
|
formatGwei,
|
|
23
|
-
serializeTransaction,
|
|
24
25
|
} from 'viem';
|
|
25
26
|
|
|
27
|
+
import { serializeSignedTransaction } from '../blob_tx.js';
|
|
26
28
|
import type { ViemClient } from '../types.js';
|
|
27
29
|
import { formatViemError } from '../utils.js';
|
|
28
30
|
import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
|
|
29
|
-
import { MAX_L1_TX_LIMIT } from './constants.js';
|
|
31
|
+
import { INSUFFICIENT_FUNDS_BACKOFF_MS, MAX_L1_TX_LIMIT } from './constants.js';
|
|
30
32
|
import type { IL1TxMetrics, IL1TxStore } from './interfaces.js';
|
|
31
|
-
import { type L1SimulationResult, ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
|
|
33
|
+
import { type L1SimulationResult, ReadOnlyL1TxUtils, isInsufficientFundsRpcError } from './readonly_l1_tx_utils.js';
|
|
32
34
|
import { Delayer, createDelayer, wrapClientWithDelayer } from './tx_delayer.js';
|
|
33
35
|
import {
|
|
34
36
|
DroppedTransactionError,
|
|
@@ -61,6 +63,11 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
61
63
|
public delayer?: Delayer;
|
|
62
64
|
/** KZG instance for blob operations. */
|
|
63
65
|
protected kzg?: BlobKzgInstance;
|
|
66
|
+
/**
|
|
67
|
+
* Sender balance and time when a send was last rejected for insufficient funds. Cleared once the balance rises above
|
|
68
|
+
* the recorded one or the backoff expires.
|
|
69
|
+
*/
|
|
70
|
+
private insufficientFunds: { balance: bigint; atMs: number } | undefined;
|
|
64
71
|
|
|
65
72
|
constructor(
|
|
66
73
|
public override client: ViemClient,
|
|
@@ -156,6 +163,47 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
156
163
|
});
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
/** Returns the sender balance recorded when a send was last rejected for insufficient funds, if any. */
|
|
167
|
+
public getInsufficientFundsAtBalance(): bigint | undefined {
|
|
168
|
+
return this.insufficientFunds?.balance;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Returns whether this sender should be skipped because a previous send was rejected for insufficient funds and its
|
|
173
|
+
* balance has not increased since. Clears the mark once the given current balance is above the recorded one, or once
|
|
174
|
+
* INSUFFICIENT_FUNDS_BACKOFF_MS have elapsed since the rejection: a rejection can come from a send whose gas limit was
|
|
175
|
+
* far above what later sends need, so a balance that did not move is not proof the publisher cannot afford them.
|
|
176
|
+
*/
|
|
177
|
+
public isBackedOffForInsufficientFunds(currentBalance: bigint): boolean {
|
|
178
|
+
if (this.insufficientFunds === undefined) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (this.hasActiveInsufficientFundsBackoff(currentBalance)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
const { balance } = this.insufficientFunds;
|
|
185
|
+
this.logger.info(`Clearing insufficient funds backoff for publisher`, {
|
|
186
|
+
account: this.getSenderAddress().toString(),
|
|
187
|
+
balance: currentBalance,
|
|
188
|
+
insufficientFundsAtBalance: balance,
|
|
189
|
+
reason: currentBalance > balance ? 'balance-increased' : 'expired',
|
|
190
|
+
});
|
|
191
|
+
this.insufficientFunds = undefined;
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Same check as {@link isBackedOffForInsufficientFunds}, but without clearing a lifted backoff or logging, so it can be
|
|
197
|
+
* used for reporting without affecting publisher selection.
|
|
198
|
+
*/
|
|
199
|
+
public hasActiveInsufficientFundsBackoff(currentBalance: bigint): boolean {
|
|
200
|
+
if (this.insufficientFunds === undefined) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const { balance, atMs } = this.insufficientFunds;
|
|
204
|
+
return currentBalance <= balance && this.dateProvider.now() - atMs < INSUFFICIENT_FUNDS_BACKOFF_MS;
|
|
205
|
+
}
|
|
206
|
+
|
|
159
207
|
/**
|
|
160
208
|
* Rehydrates transaction states from the store and resumes monitoring for pending transactions.
|
|
161
209
|
* This should be called on startup to restore state and resume monitoring of any in-flight transactions.
|
|
@@ -220,11 +268,12 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
220
268
|
|
|
221
269
|
private async signTransaction(txRequest: TransactionSerializable): Promise<`0x${string}`> {
|
|
222
270
|
const signature = await this.signer(txRequest, this.getSenderAddress());
|
|
223
|
-
return
|
|
271
|
+
return serializeSignedTransaction(txRequest, signature);
|
|
224
272
|
}
|
|
225
273
|
|
|
226
274
|
protected async prepareSignedTransaction(txData: PrepareTransactionRequestRequest) {
|
|
227
|
-
|
|
275
|
+
// Passing the chain id keeps viem from routing a fully specified request through eth_fillTransaction.
|
|
276
|
+
const txRequest = await this.client.prepareTransactionRequest({ chainId: this.client.chain.id, ...txData });
|
|
228
277
|
return await this.signTransaction(txRequest as TransactionSerializable);
|
|
229
278
|
}
|
|
230
279
|
|
|
@@ -340,6 +389,9 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
340
389
|
|
|
341
390
|
return { txHash, state: l1TxState };
|
|
342
391
|
} catch (err: any) {
|
|
392
|
+
if (isInsufficientFundsRpcError(err) || getErrorCause(err, InsufficientFundsError)) {
|
|
393
|
+
await this.recordInsufficientFunds();
|
|
394
|
+
}
|
|
343
395
|
const viemError = formatViemError(err, request.abi);
|
|
344
396
|
this.logger.error(`Failed to send L1 transaction: ${viemError.message}`, viemError, {
|
|
345
397
|
request: pick(request, 'to', 'value'),
|
|
@@ -348,6 +400,21 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
348
400
|
}
|
|
349
401
|
}
|
|
350
402
|
|
|
403
|
+
/** Records the current sender balance as one that cannot afford a send, so selection skips it for a while. */
|
|
404
|
+
private async recordInsufficientFunds(): Promise<void> {
|
|
405
|
+
try {
|
|
406
|
+
const balance = await this.getSenderBalance();
|
|
407
|
+
this.insufficientFunds = { balance, atMs: this.dateProvider.now() };
|
|
408
|
+
this.logger.warn(`L1 send rejected for insufficient funds, backing off publisher until its balance increases`, {
|
|
409
|
+
account: this.getSenderAddress().toString(),
|
|
410
|
+
balance,
|
|
411
|
+
backoffMs: INSUFFICIENT_FUNDS_BACKOFF_MS,
|
|
412
|
+
});
|
|
413
|
+
} catch (err) {
|
|
414
|
+
this.logger.warn(`Failed to read balance after an insufficient funds rejection`, { err });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
351
418
|
private async tryGetTxReceipt(
|
|
352
419
|
txHashes: Hex[],
|
|
353
420
|
nonce: number,
|
|
@@ -51,7 +51,7 @@ const INSUFFICIENT_FUNDS_RPC_ERROR_CODE = -38014;
|
|
|
51
51
|
* error code, since RPC gateways differ in how they wrap node errors. Apply this only to transport errors: an
|
|
52
52
|
* execution revert string can mention insufficient funds without being this kind of rejection.
|
|
53
53
|
*/
|
|
54
|
-
function isInsufficientFundsRpcError(err: unknown): boolean {
|
|
54
|
+
export function isInsufficientFundsRpcError(err: unknown): boolean {
|
|
55
55
|
if (getL1RpcErrorCode(err) === INSUFFICIENT_FUNDS_RPC_ERROR_CODE) {
|
|
56
56
|
return true;
|
|
57
57
|
}
|