@chainsafe/lodestar 1.35.0-dev.8689cc3545 → 1.35.0-dev.8b45b1e978
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/.git-data.json +1 -1
- package/package.json +18 -19
- package/bin/lodestar.js +0 -3
- package/bin/lodestar.ts +0 -3
- package/lib/applyPreset.d.ts.map +0 -1
- package/lib/cli.d.ts.map +0 -1
- package/lib/cmds/beacon/handler.d.ts.map +0 -1
- package/lib/cmds/beacon/index.d.ts.map +0 -1
- package/lib/cmds/beacon/initBeaconState.d.ts.map +0 -1
- package/lib/cmds/beacon/initPeerIdAndEnr.d.ts.map +0 -1
- package/lib/cmds/beacon/options.d.ts.map +0 -1
- package/lib/cmds/beacon/paths.d.ts.map +0 -1
- package/lib/cmds/bootnode/handler.d.ts.map +0 -1
- package/lib/cmds/bootnode/index.d.ts.map +0 -1
- package/lib/cmds/bootnode/options.d.ts.map +0 -1
- package/lib/cmds/dev/files.d.ts.map +0 -1
- package/lib/cmds/dev/handler.d.ts.map +0 -1
- package/lib/cmds/dev/index.d.ts.map +0 -1
- package/lib/cmds/dev/options.d.ts.map +0 -1
- package/lib/cmds/index.d.ts.map +0 -1
- package/lib/cmds/lightclient/handler.d.ts.map +0 -1
- package/lib/cmds/lightclient/index.d.ts.map +0 -1
- package/lib/cmds/lightclient/options.d.ts.map +0 -1
- package/lib/cmds/validator/blsToExecutionChange.d.ts.map +0 -1
- package/lib/cmds/validator/handler.d.ts.map +0 -1
- package/lib/cmds/validator/import.d.ts.map +0 -1
- package/lib/cmds/validator/index.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/decryptKeystoreDefinitions.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/decryptKeystores/index.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/decryptKeystores/poolSize.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/decryptKeystores/threadPool.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/decryptKeystores/types.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/decryptKeystores/worker.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/impl.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/interface.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/keystoreCache.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/persistedKeys.d.ts.map +0 -1
- package/lib/cmds/validator/keymanager/server.d.ts.map +0 -1
- package/lib/cmds/validator/list.d.ts.map +0 -1
- package/lib/cmds/validator/options.d.ts.map +0 -1
- package/lib/cmds/validator/paths.d.ts.map +0 -1
- package/lib/cmds/validator/signers/importExternalKeystores.d.ts.map +0 -1
- package/lib/cmds/validator/signers/index.d.ts.map +0 -1
- package/lib/cmds/validator/signers/logSigners.d.ts.map +0 -1
- package/lib/cmds/validator/slashingProtection/export.d.ts.map +0 -1
- package/lib/cmds/validator/slashingProtection/import.d.ts.map +0 -1
- package/lib/cmds/validator/slashingProtection/index.d.ts.map +0 -1
- package/lib/cmds/validator/slashingProtection/options.d.ts.map +0 -1
- package/lib/cmds/validator/slashingProtection/utils.d.ts.map +0 -1
- package/lib/cmds/validator/voluntaryExit.d.ts.map +0 -1
- package/lib/config/beaconNodeOptions.d.ts.map +0 -1
- package/lib/config/beaconParams.d.ts.map +0 -1
- package/lib/config/index.d.ts.map +0 -1
- package/lib/config/peerId.d.ts.map +0 -1
- package/lib/config/types.d.ts.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/migrations/index.d.ts.map +0 -1
- package/lib/networks/chiado.d.ts.map +0 -1
- package/lib/networks/dev.d.ts.map +0 -1
- package/lib/networks/ephemery.d.ts.map +0 -1
- package/lib/networks/gnosis.d.ts.map +0 -1
- package/lib/networks/holesky.d.ts.map +0 -1
- package/lib/networks/hoodi.d.ts.map +0 -1
- package/lib/networks/index.d.ts.map +0 -1
- package/lib/networks/mainnet.d.ts.map +0 -1
- package/lib/networks/sepolia.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/api.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/builder.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/chain.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/eth1.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/execution.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/index.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/metrics.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/monitoring.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/network.d.ts.map +0 -1
- package/lib/options/beaconNodeOptions/sync.d.ts.map +0 -1
- package/lib/options/globalOptions.d.ts.map +0 -1
- package/lib/options/index.d.ts.map +0 -1
- package/lib/options/logOptions.d.ts.map +0 -1
- package/lib/options/paramsOptions.d.ts.map +0 -1
- package/lib/paths/global.d.ts.map +0 -1
- package/lib/paths/rootDir.d.ts.map +0 -1
- package/lib/util/errors.d.ts.map +0 -1
- package/lib/util/ethers.d.ts.map +0 -1
- package/lib/util/feeRecipient.d.ts.map +0 -1
- package/lib/util/file.d.ts.map +0 -1
- package/lib/util/format.d.ts.map +0 -1
- package/lib/util/fs.d.ts.map +0 -1
- package/lib/util/gitData/gitDataPath.d.ts.map +0 -1
- package/lib/util/gitData/index.d.ts.map +0 -1
- package/lib/util/gitData/writeGitData.d.ts.map +0 -1
- package/lib/util/index.d.ts.map +0 -1
- package/lib/util/jwt.d.ts.map +0 -1
- package/lib/util/lockfile.d.ts.map +0 -1
- package/lib/util/logger.d.ts.map +0 -1
- package/lib/util/object.d.ts.map +0 -1
- package/lib/util/passphrase.d.ts.map +0 -1
- package/lib/util/process.d.ts.map +0 -1
- package/lib/util/progress.d.ts.map +0 -1
- package/lib/util/proposerConfig.d.ts.map +0 -1
- package/lib/util/pruneOldFilesInDir.d.ts.map +0 -1
- package/lib/util/sleep.d.ts.map +0 -1
- package/lib/util/stripOffNewlines.d.ts.map +0 -1
- package/lib/util/types.d.ts.map +0 -1
- package/lib/util/version.d.ts.map +0 -1
- package/src/applyPreset.ts +0 -91
- package/src/cli.ts +0 -56
- package/src/cmds/beacon/handler.ts +0 -267
- package/src/cmds/beacon/index.ts +0 -18
- package/src/cmds/beacon/initBeaconState.ts +0 -275
- package/src/cmds/beacon/initPeerIdAndEnr.ts +0 -199
- package/src/cmds/beacon/options.ts +0 -214
- package/src/cmds/beacon/paths.ts +0 -62
- package/src/cmds/bootnode/handler.ts +0 -203
- package/src/cmds/bootnode/index.ts +0 -13
- package/src/cmds/bootnode/options.ts +0 -109
- package/src/cmds/dev/files.ts +0 -52
- package/src/cmds/dev/handler.ts +0 -86
- package/src/cmds/dev/index.ts +0 -18
- package/src/cmds/dev/options.ts +0 -110
- package/src/cmds/index.ts +0 -15
- package/src/cmds/lightclient/handler.ts +0 -36
- package/src/cmds/lightclient/index.ts +0 -18
- package/src/cmds/lightclient/options.ts +0 -21
- package/src/cmds/validator/blsToExecutionChange.ts +0 -91
- package/src/cmds/validator/handler.ts +0 -300
- package/src/cmds/validator/import.ts +0 -111
- package/src/cmds/validator/index.ts +0 -28
- package/src/cmds/validator/keymanager/decryptKeystoreDefinitions.ts +0 -189
- package/src/cmds/validator/keymanager/decryptKeystores/index.ts +0 -1
- package/src/cmds/validator/keymanager/decryptKeystores/poolSize.ts +0 -16
- package/src/cmds/validator/keymanager/decryptKeystores/threadPool.ts +0 -75
- package/src/cmds/validator/keymanager/decryptKeystores/types.ts +0 -12
- package/src/cmds/validator/keymanager/decryptKeystores/worker.ts +0 -24
- package/src/cmds/validator/keymanager/impl.ts +0 -425
- package/src/cmds/validator/keymanager/interface.ts +0 -35
- package/src/cmds/validator/keymanager/keystoreCache.ts +0 -91
- package/src/cmds/validator/keymanager/persistedKeys.ts +0 -268
- package/src/cmds/validator/keymanager/server.ts +0 -86
- package/src/cmds/validator/list.ts +0 -35
- package/src/cmds/validator/options.ts +0 -461
- package/src/cmds/validator/paths.ts +0 -95
- package/src/cmds/validator/signers/importExternalKeystores.ts +0 -69
- package/src/cmds/validator/signers/index.ts +0 -176
- package/src/cmds/validator/signers/logSigners.ts +0 -81
- package/src/cmds/validator/slashingProtection/export.ts +0 -110
- package/src/cmds/validator/slashingProtection/import.ts +0 -70
- package/src/cmds/validator/slashingProtection/index.ts +0 -12
- package/src/cmds/validator/slashingProtection/options.ts +0 -15
- package/src/cmds/validator/slashingProtection/utils.ts +0 -56
- package/src/cmds/validator/voluntaryExit.ts +0 -232
- package/src/config/beaconNodeOptions.ts +0 -68
- package/src/config/beaconParams.ts +0 -87
- package/src/config/index.ts +0 -3
- package/src/config/peerId.ts +0 -50
- package/src/config/types.ts +0 -3
- package/src/index.ts +0 -28
- package/src/migrations/index.ts +0 -0
- package/src/networks/chiado.ts +0 -20
- package/src/networks/dev.ts +0 -27
- package/src/networks/ephemery.ts +0 -9
- package/src/networks/gnosis.ts +0 -18
- package/src/networks/holesky.ts +0 -17
- package/src/networks/hoodi.ts +0 -16
- package/src/networks/index.ts +0 -236
- package/src/networks/mainnet.ts +0 -34
- package/src/networks/sepolia.ts +0 -17
- package/src/options/beaconNodeOptions/api.ts +0 -110
- package/src/options/beaconNodeOptions/builder.ts +0 -63
- package/src/options/beaconNodeOptions/chain.ts +0 -326
- package/src/options/beaconNodeOptions/eth1.ts +0 -95
- package/src/options/beaconNodeOptions/execution.ts +0 -92
- package/src/options/beaconNodeOptions/index.ts +0 -50
- package/src/options/beaconNodeOptions/metrics.ts +0 -39
- package/src/options/beaconNodeOptions/monitoring.ts +0 -61
- package/src/options/beaconNodeOptions/network.ts +0 -401
- package/src/options/beaconNodeOptions/sync.ts +0 -65
- package/src/options/globalOptions.ts +0 -72
- package/src/options/index.ts +0 -3
- package/src/options/logOptions.ts +0 -70
- package/src/options/paramsOptions.ts +0 -72
- package/src/paths/global.ts +0 -24
- package/src/paths/rootDir.ts +0 -11
- package/src/util/errors.ts +0 -20
- package/src/util/ethers.ts +0 -44
- package/src/util/feeRecipient.ts +0 -6
- package/src/util/file.ts +0 -167
- package/src/util/format.ts +0 -76
- package/src/util/fs.ts +0 -59
- package/src/util/gitData/gitDataPath.ts +0 -48
- package/src/util/gitData/index.ts +0 -70
- package/src/util/gitData/writeGitData.ts +0 -10
- package/src/util/index.ts +0 -17
- package/src/util/jwt.ts +0 -10
- package/src/util/lockfile.ts +0 -45
- package/src/util/logger.ts +0 -105
- package/src/util/object.ts +0 -15
- package/src/util/passphrase.ts +0 -25
- package/src/util/process.ts +0 -25
- package/src/util/progress.ts +0 -58
- package/src/util/proposerConfig.ts +0 -136
- package/src/util/pruneOldFilesInDir.ts +0 -27
- package/src/util/sleep.ts +0 -3
- package/src/util/stripOffNewlines.ts +0 -6
- package/src/util/types.ts +0 -8
- package/src/util/version.ts +0 -74
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import {getClient} from "@lodestar/api";
|
|
3
|
-
import {Lightclient} from "@lodestar/light-client";
|
|
4
|
-
import {LightClientRestTransport} from "@lodestar/light-client/transport";
|
|
5
|
-
import {getNodeLogger} from "@lodestar/logger/node";
|
|
6
|
-
import {fromHex} from "@lodestar/utils";
|
|
7
|
-
import {getBeaconConfigFromArgs} from "../../config/beaconParams.js";
|
|
8
|
-
import {GlobalArgs} from "../../options/index.js";
|
|
9
|
-
import {getGlobalPaths} from "../../paths/global.js";
|
|
10
|
-
import {parseLoggerArgs} from "../../util/logger.js";
|
|
11
|
-
import {ILightClientArgs} from "./options.js";
|
|
12
|
-
|
|
13
|
-
export async function lightclientHandler(args: ILightClientArgs & GlobalArgs): Promise<void> {
|
|
14
|
-
const {config, network} = getBeaconConfigFromArgs(args);
|
|
15
|
-
const globalPaths = getGlobalPaths(args, network);
|
|
16
|
-
|
|
17
|
-
const logger = getNodeLogger(
|
|
18
|
-
parseLoggerArgs(args, {defaultLogFilepath: path.join(globalPaths.dataDir, "lightclient.log")}, config)
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
const api = getClient({baseUrl: args.beaconApiUrl}, {config});
|
|
22
|
-
const {genesisTime, genesisValidatorsRoot} = (await api.beacon.getGenesis()).value();
|
|
23
|
-
|
|
24
|
-
const client = await Lightclient.initializeFromCheckpointRoot({
|
|
25
|
-
config,
|
|
26
|
-
logger,
|
|
27
|
-
genesisData: {
|
|
28
|
-
genesisTime,
|
|
29
|
-
genesisValidatorsRoot,
|
|
30
|
-
},
|
|
31
|
-
checkpointRoot: fromHex(args.checkpointRoot),
|
|
32
|
-
transport: new LightClientRestTransport(api),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
void client.start();
|
|
36
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import {CliCommand} from "@lodestar/utils";
|
|
2
|
-
import {GlobalArgs} from "../../options/index.js";
|
|
3
|
-
import {lightclientHandler} from "./handler.js";
|
|
4
|
-
import {ILightClientArgs, lightclientOptions} from "./options.js";
|
|
5
|
-
|
|
6
|
-
export const lightclient: CliCommand<ILightClientArgs, GlobalArgs> = {
|
|
7
|
-
command: "lightclient",
|
|
8
|
-
describe: "Run lightclient",
|
|
9
|
-
docsFolder: "libraries/lightclient-prover",
|
|
10
|
-
examples: [
|
|
11
|
-
{
|
|
12
|
-
command: "lightclient --network hoodi",
|
|
13
|
-
description: "Run lightclient with hoodi network",
|
|
14
|
-
},
|
|
15
|
-
],
|
|
16
|
-
options: lightclientOptions,
|
|
17
|
-
handler: lightclientHandler,
|
|
18
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import {CliCommandOptions} from "@lodestar/utils";
|
|
2
|
-
import {LogArgs, logOptions} from "../../options/logOptions.js";
|
|
3
|
-
|
|
4
|
-
export type ILightClientArgs = LogArgs & {
|
|
5
|
-
beaconApiUrl: string;
|
|
6
|
-
checkpointRoot: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const lightclientOptions: CliCommandOptions<ILightClientArgs> = {
|
|
10
|
-
...logOptions,
|
|
11
|
-
beaconApiUrl: {
|
|
12
|
-
description: "Url to a beacon node that support lightclient API",
|
|
13
|
-
type: "string",
|
|
14
|
-
demandOption: true,
|
|
15
|
-
},
|
|
16
|
-
checkpointRoot: {
|
|
17
|
-
description: "Checkpoint root hex string to sync the lightclient from, start with 0x",
|
|
18
|
-
type: "string",
|
|
19
|
-
demandOption: true,
|
|
20
|
-
},
|
|
21
|
-
};
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import {SecretKey} from "@chainsafe/blst";
|
|
2
|
-
import {getClient} from "@lodestar/api";
|
|
3
|
-
import {createBeaconConfig} from "@lodestar/config";
|
|
4
|
-
import {DOMAIN_BLS_TO_EXECUTION_CHANGE, ForkName} from "@lodestar/params";
|
|
5
|
-
import {computeSigningRoot} from "@lodestar/state-transition";
|
|
6
|
-
import {capella, ssz} from "@lodestar/types";
|
|
7
|
-
import {CliCommand, fromHex} from "@lodestar/utils";
|
|
8
|
-
import {getBeaconConfigFromArgs} from "../../config/index.js";
|
|
9
|
-
import {GlobalArgs} from "../../options/index.js";
|
|
10
|
-
import {IValidatorCliArgs} from "./options.js";
|
|
11
|
-
|
|
12
|
-
type BlsToExecutionChangeArgs = {
|
|
13
|
-
publicKey: string;
|
|
14
|
-
fromBlsPrivkey: string;
|
|
15
|
-
toExecutionAddress: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const blsToExecutionChange: CliCommand<BlsToExecutionChangeArgs, IValidatorCliArgs & GlobalArgs> = {
|
|
19
|
-
command: "bls-to-execution-change",
|
|
20
|
-
|
|
21
|
-
describe:
|
|
22
|
-
"Performs BLS To Execution Change for a given validator (as identified via `publicKey`. \
|
|
23
|
-
If no `publicKey` is provided, a prompt will ask the user which validator they would \
|
|
24
|
-
like to choose for BLS To Execution Change.",
|
|
25
|
-
|
|
26
|
-
examples: [
|
|
27
|
-
{
|
|
28
|
-
command: "validator bls-to-execution-change --publicKey 0xF00 --fromBlsPrivkey ... --toExecutionAddress ...",
|
|
29
|
-
description: "Perform BLS To Execution Change for the validator who has a public key 0xF00",
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
|
|
33
|
-
options: {
|
|
34
|
-
publicKey: {
|
|
35
|
-
description: "Validator public key for which to set withdrawal address hence enabling withdrawals",
|
|
36
|
-
type: "string",
|
|
37
|
-
demandOption: true,
|
|
38
|
-
},
|
|
39
|
-
fromBlsPrivkey: {
|
|
40
|
-
description: "Bls withdrawals private key to sign the message",
|
|
41
|
-
type: "string",
|
|
42
|
-
demandOption: true,
|
|
43
|
-
},
|
|
44
|
-
toExecutionAddress: {
|
|
45
|
-
description: "Address to which the validator's balances will be set to be withdrawn.",
|
|
46
|
-
type: "string",
|
|
47
|
-
demandOption: true,
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
handler: async (args) => {
|
|
52
|
-
const {publicKey} = args;
|
|
53
|
-
// Fetch genesisValidatorsRoot always from beacon node as anyway beacon node is needed for
|
|
54
|
-
// submitting the signed message
|
|
55
|
-
const {config: chainForkConfig} = getBeaconConfigFromArgs(args);
|
|
56
|
-
const client = getClient({urls: args.beaconNodes}, {config: chainForkConfig});
|
|
57
|
-
const {genesisValidatorsRoot} = (await client.beacon.getGenesis()).value();
|
|
58
|
-
const config = createBeaconConfig(chainForkConfig, genesisValidatorsRoot);
|
|
59
|
-
|
|
60
|
-
const validators = (await client.beacon.postStateValidators({stateId: "head", validatorIds: [publicKey]})).value();
|
|
61
|
-
const validator = validators[0];
|
|
62
|
-
if (validator === undefined) {
|
|
63
|
-
throw new Error(`Validator pubkey ${publicKey} not found in state`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const blsPrivkey = SecretKey.fromBytes(fromHex(args.fromBlsPrivkey));
|
|
67
|
-
const fromBlsPubkey = blsPrivkey.toPublicKey().toBytes();
|
|
68
|
-
|
|
69
|
-
const blsToExecutionChange: capella.BLSToExecutionChange = {
|
|
70
|
-
validatorIndex: validator.index,
|
|
71
|
-
fromBlsPubkey,
|
|
72
|
-
toExecutionAddress: fromHex(args.toExecutionAddress),
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const signatureFork = ForkName.phase0;
|
|
76
|
-
const domain = config.getDomainAtFork(signatureFork, DOMAIN_BLS_TO_EXECUTION_CHANGE);
|
|
77
|
-
const signingRoot = computeSigningRoot(ssz.capella.BLSToExecutionChange, blsToExecutionChange, domain);
|
|
78
|
-
const signedBLSToExecutionChange = {
|
|
79
|
-
message: blsToExecutionChange,
|
|
80
|
-
signature: blsPrivkey.sign(signingRoot).toBytes(),
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
await client.beacon.submitPoolBLSToExecutionChange({
|
|
85
|
-
blsToExecutionChanges: [signedBLSToExecutionChange],
|
|
86
|
-
})
|
|
87
|
-
).assertOk();
|
|
88
|
-
|
|
89
|
-
console.log(`Submitted bls to execution change for ${publicKey}`);
|
|
90
|
-
},
|
|
91
|
-
};
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import {setMaxListeners} from "node:events";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {WireFormat, routes} from "@lodestar/api";
|
|
4
|
-
import {
|
|
5
|
-
MonitoringService,
|
|
6
|
-
RegistryMetricCreator,
|
|
7
|
-
collectNodeJSMetrics,
|
|
8
|
-
getHttpMetricsServer,
|
|
9
|
-
} from "@lodestar/beacon-node";
|
|
10
|
-
import {LevelDbController} from "@lodestar/db";
|
|
11
|
-
import {getNodeLogger} from "@lodestar/logger/node";
|
|
12
|
-
import {
|
|
13
|
-
ProcessShutdownCallback,
|
|
14
|
-
SlashingProtection,
|
|
15
|
-
Validator,
|
|
16
|
-
ValidatorProposerConfig,
|
|
17
|
-
defaultOptions,
|
|
18
|
-
getMetrics,
|
|
19
|
-
} from "@lodestar/validator";
|
|
20
|
-
import {getBeaconConfigFromArgs} from "../../config/index.js";
|
|
21
|
-
import {GlobalArgs} from "../../options/index.js";
|
|
22
|
-
import {
|
|
23
|
-
YargsError,
|
|
24
|
-
cleanOldLogFiles,
|
|
25
|
-
mkdir,
|
|
26
|
-
onGracefulShutdown,
|
|
27
|
-
parseFeeRecipient,
|
|
28
|
-
parseLoggerArgs,
|
|
29
|
-
parseProposerConfig,
|
|
30
|
-
} from "../../util/index.js";
|
|
31
|
-
import {parseBuilderBoostFactor, parseBuilderSelection} from "../../util/proposerConfig.js";
|
|
32
|
-
import {getVersionData} from "../../util/version.js";
|
|
33
|
-
import {KeymanagerApi} from "./keymanager/impl.js";
|
|
34
|
-
import {IPersistedKeysBackend} from "./keymanager/interface.js";
|
|
35
|
-
import {PersistedKeysBackend} from "./keymanager/persistedKeys.js";
|
|
36
|
-
import {KeymanagerRestApiServer} from "./keymanager/server.js";
|
|
37
|
-
import {IValidatorCliArgs, validatorMetricsDefaultOptions, validatorMonitoringDefaultOptions} from "./options.js";
|
|
38
|
-
import {getAccountPaths, getValidatorPaths} from "./paths.js";
|
|
39
|
-
import {getSignersFromArgs} from "./signers/index.js";
|
|
40
|
-
import {logSigners, warnOrExitNoSigners} from "./signers/logSigners.js";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Runs a validator client.
|
|
44
|
-
*/
|
|
45
|
-
export async function validatorHandler(args: IValidatorCliArgs & GlobalArgs): Promise<void> {
|
|
46
|
-
const {config, network} = getBeaconConfigFromArgs(args);
|
|
47
|
-
|
|
48
|
-
const {doppelgangerProtection} = args;
|
|
49
|
-
|
|
50
|
-
const validatorPaths = getValidatorPaths(args, network);
|
|
51
|
-
const accountPaths = getAccountPaths(args, network);
|
|
52
|
-
|
|
53
|
-
const defaultLogFilepath = path.join(validatorPaths.dataDir, "validator.log");
|
|
54
|
-
const logger = getNodeLogger(parseLoggerArgs(args, {defaultLogFilepath}, config));
|
|
55
|
-
try {
|
|
56
|
-
cleanOldLogFiles(args, {defaultLogFilepath});
|
|
57
|
-
} catch (e) {
|
|
58
|
-
logger.debug("Not able to delete log files", {}, e as Error);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const persistedKeysBackend = new PersistedKeysBackend(accountPaths);
|
|
62
|
-
const valProposerConfig = getProposerConfigFromArgs(args, {persistedKeysBackend, accountPaths});
|
|
63
|
-
|
|
64
|
-
const {version, commit} = getVersionData();
|
|
65
|
-
logger.info("Lodestar", {network, version, commit});
|
|
66
|
-
if (args.distributed) logger.info("Client is configured to run as part of a distributed validator cluster");
|
|
67
|
-
logger.info("Connecting to LevelDB database", {path: validatorPaths.validatorsDbDir});
|
|
68
|
-
|
|
69
|
-
const dbPath = validatorPaths.validatorsDbDir;
|
|
70
|
-
mkdir(dbPath);
|
|
71
|
-
|
|
72
|
-
const onGracefulShutdownCbs: (() => Promise<void> | void)[] = [];
|
|
73
|
-
onGracefulShutdown(async () => {
|
|
74
|
-
for (const cb of onGracefulShutdownCbs) await cb();
|
|
75
|
-
}, logger.info.bind(logger));
|
|
76
|
-
|
|
77
|
-
// Callback for validator to request forced exit, in case of doppelganger detection
|
|
78
|
-
const processShutdownCallback: ProcessShutdownCallback = (err) => {
|
|
79
|
-
logger.error("Process shutdown requested", {}, err);
|
|
80
|
-
process.kill(process.pid, "SIGINT");
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// This AbortController interrupts various validators ops: genesis req, clients call, clock etc
|
|
84
|
-
const abortController = new AbortController();
|
|
85
|
-
|
|
86
|
-
// We set infinity for abort controller used for validator operations,
|
|
87
|
-
// to prevent MaxListenersExceededWarning which get logged when listeners > 10
|
|
88
|
-
// Since it is perfectly fine to have listeners > 10
|
|
89
|
-
setMaxListeners(Infinity, abortController.signal);
|
|
90
|
-
|
|
91
|
-
onGracefulShutdownCbs.push(async () => abortController.abort());
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* For rationale and documentation of how signers are loaded from args and disk,
|
|
95
|
-
* see {@link PersistedKeysBackend} and {@link getSignersFromArgs}
|
|
96
|
-
*
|
|
97
|
-
* Note: local signers are already locked once returned from this function.
|
|
98
|
-
*/
|
|
99
|
-
const signers = await getSignersFromArgs(args, network, {logger, signal: abortController.signal});
|
|
100
|
-
|
|
101
|
-
// Ensure the validator has at least one key
|
|
102
|
-
if (signers.length === 0) {
|
|
103
|
-
warnOrExitNoSigners(args, logger);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
logSigners(logger, signers);
|
|
107
|
-
|
|
108
|
-
const db = await LevelDbController.create({name: dbPath}, {metrics: null, logger});
|
|
109
|
-
onGracefulShutdownCbs.push(() => db.close());
|
|
110
|
-
|
|
111
|
-
const slashingProtection = new SlashingProtection(db);
|
|
112
|
-
|
|
113
|
-
// Create metrics registry if metrics are enabled or monitoring endpoint is configured
|
|
114
|
-
// Send version and network data for static registries
|
|
115
|
-
|
|
116
|
-
const register = args.metrics || args["monitoring.endpoint"] ? new RegistryMetricCreator() : null;
|
|
117
|
-
const metrics = register && getMetrics(register, {version, commit, network});
|
|
118
|
-
|
|
119
|
-
// Start metrics server if metrics are enabled.
|
|
120
|
-
// Collect NodeJS metrics defined in the Lodestar repo
|
|
121
|
-
|
|
122
|
-
if (metrics) {
|
|
123
|
-
const closeMetrics = collectNodeJSMetrics(register);
|
|
124
|
-
onGracefulShutdownCbs.push(() => closeMetrics());
|
|
125
|
-
|
|
126
|
-
// only start server if metrics are explicitly enabled
|
|
127
|
-
if (args.metrics) {
|
|
128
|
-
const port = args["metrics.port"] ?? validatorMetricsDefaultOptions.port;
|
|
129
|
-
const address = args["metrics.address"] ?? validatorMetricsDefaultOptions.address;
|
|
130
|
-
const metricsServer = await getHttpMetricsServer({port, address}, {register, logger});
|
|
131
|
-
|
|
132
|
-
onGracefulShutdownCbs.push(() => metricsServer.close());
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (args["monitoring.endpoint"]) {
|
|
137
|
-
const {interval, initialDelay, requestTimeout, collectSystemStats} = validatorMonitoringDefaultOptions;
|
|
138
|
-
|
|
139
|
-
const monitoring = new MonitoringService(
|
|
140
|
-
"validator",
|
|
141
|
-
{
|
|
142
|
-
endpoint: args["monitoring.endpoint"],
|
|
143
|
-
interval: args["monitoring.interval"] ?? interval,
|
|
144
|
-
initialDelay: args["monitoring.initialDelay"] ?? initialDelay,
|
|
145
|
-
requestTimeout: args["monitoring.requestTimeout"] ?? requestTimeout,
|
|
146
|
-
collectSystemStats: args["monitoring.collectSystemStats"] ?? collectSystemStats,
|
|
147
|
-
},
|
|
148
|
-
{register: register as RegistryMetricCreator, logger}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
onGracefulShutdownCbs.push(() => monitoring.close());
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// This promise resolves once genesis is available.
|
|
155
|
-
// It will wait for genesis, so this promise can be potentially very long
|
|
156
|
-
|
|
157
|
-
const validator = await Validator.initializeFromBeaconNode(
|
|
158
|
-
{
|
|
159
|
-
db,
|
|
160
|
-
config,
|
|
161
|
-
slashingProtection,
|
|
162
|
-
api: {
|
|
163
|
-
clientOrUrls: args.beaconNodes,
|
|
164
|
-
globalInit: {
|
|
165
|
-
requestWireFormat: parseWireFormat(args, "http.requestWireFormat"),
|
|
166
|
-
responseWireFormat: parseWireFormat(args, "http.responseWireFormat"),
|
|
167
|
-
headers: {"User-Agent": `Lodestar/${version}`},
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
logger,
|
|
171
|
-
processShutdownCallback,
|
|
172
|
-
signers,
|
|
173
|
-
abortController,
|
|
174
|
-
doppelgangerProtection,
|
|
175
|
-
afterBlockDelaySlotFraction: args.afterBlockDelaySlotFraction,
|
|
176
|
-
scAfterBlockDelaySlotFraction: args.scAfterBlockDelaySlotFraction,
|
|
177
|
-
valProposerConfig,
|
|
178
|
-
distributed: args.distributed,
|
|
179
|
-
broadcastValidation: parseBroadcastValidation(args.broadcastValidation),
|
|
180
|
-
blindedLocal: args.blindedLocal,
|
|
181
|
-
externalSigner: {
|
|
182
|
-
url: args["externalSigner.url"],
|
|
183
|
-
fetch: args["externalSigner.fetch"],
|
|
184
|
-
fetchInterval: args["externalSigner.fetchInterval"],
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
metrics
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
onGracefulShutdownCbs.push(() => validator.close());
|
|
191
|
-
|
|
192
|
-
// Start keymanager API backend
|
|
193
|
-
// Only if keymanagerEnabled flag is set to true
|
|
194
|
-
if (args.keymanager) {
|
|
195
|
-
// if proposerSettingsFile provided disable the key proposerConfigWrite in keymanager
|
|
196
|
-
const proposerConfigWriteDisabled = args.proposerSettingsFile !== undefined;
|
|
197
|
-
if (proposerConfigWriteDisabled) {
|
|
198
|
-
logger.warn(
|
|
199
|
-
"Proposer data updates (feeRecipient/gasLimit etc) will not be available via Keymanager API as proposerSettingsFile has been set"
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const keymanagerApi = new KeymanagerApi(
|
|
204
|
-
validator,
|
|
205
|
-
persistedKeysBackend,
|
|
206
|
-
abortController.signal,
|
|
207
|
-
proposerConfigWriteDisabled
|
|
208
|
-
);
|
|
209
|
-
const keymanagerServer = new KeymanagerRestApiServer(
|
|
210
|
-
{
|
|
211
|
-
address: args["keymanager.address"],
|
|
212
|
-
port: args["keymanager.port"],
|
|
213
|
-
cors: args["keymanager.cors"],
|
|
214
|
-
isAuthEnabled: args["keymanager.auth"],
|
|
215
|
-
headerLimit: args["keymanager.headerLimit"],
|
|
216
|
-
bodyLimit: args["keymanager.bodyLimit"],
|
|
217
|
-
stacktraces: args["keymanager.stacktraces"],
|
|
218
|
-
tokenFile: args["keymanager.tokenFile"],
|
|
219
|
-
tokenDir: dbPath,
|
|
220
|
-
},
|
|
221
|
-
{config, logger, api: keymanagerApi, metrics: metrics ? metrics.keymanagerApiRest : null}
|
|
222
|
-
);
|
|
223
|
-
onGracefulShutdownCbs.push(() => keymanagerServer.close());
|
|
224
|
-
await keymanagerServer.listen();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function getProposerConfigFromArgs(
|
|
229
|
-
args: IValidatorCliArgs,
|
|
230
|
-
{
|
|
231
|
-
persistedKeysBackend,
|
|
232
|
-
accountPaths,
|
|
233
|
-
}: {persistedKeysBackend: IPersistedKeysBackend; accountPaths: {proposerDir: string}}
|
|
234
|
-
): ValidatorProposerConfig {
|
|
235
|
-
const defaultConfig = {
|
|
236
|
-
graffiti: args.graffiti,
|
|
237
|
-
strictFeeRecipientCheck: args.strictFeeRecipientCheck,
|
|
238
|
-
feeRecipient: args.suggestedFeeRecipient ? parseFeeRecipient(args.suggestedFeeRecipient) : undefined,
|
|
239
|
-
builder: {
|
|
240
|
-
gasLimit: args.defaultGasLimit,
|
|
241
|
-
selection: parseBuilderSelection(
|
|
242
|
-
args["builder.selection"] ?? (args.builder ? defaultOptions.builderAliasSelection : undefined)
|
|
243
|
-
),
|
|
244
|
-
boostFactor: parseBuilderBoostFactor(args["builder.boostFactor"]),
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
let valProposerConfig: ValidatorProposerConfig;
|
|
249
|
-
const proposerConfigFromKeymanager = persistedKeysBackend.readProposerConfigs();
|
|
250
|
-
|
|
251
|
-
if (Object.keys(proposerConfigFromKeymanager).length > 0) {
|
|
252
|
-
// from persistedBackend
|
|
253
|
-
if (args.proposerSettingsFile) {
|
|
254
|
-
throw new YargsError(
|
|
255
|
-
`Cannot accept --proposerSettingsFile since it conflicts with proposer configs previously persisted via the keymanager api. Delete directory ${accountPaths.proposerDir} to discard them`
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
valProposerConfig = {proposerConfig: proposerConfigFromKeymanager, defaultConfig};
|
|
259
|
-
} else {
|
|
260
|
-
// from Proposer Settings File
|
|
261
|
-
if (args.proposerSettingsFile) {
|
|
262
|
-
// parseProposerConfig will override the defaults with the arg created defaultConfig
|
|
263
|
-
valProposerConfig = parseProposerConfig(args.proposerSettingsFile, defaultConfig);
|
|
264
|
-
} else {
|
|
265
|
-
valProposerConfig = {defaultConfig} as ValidatorProposerConfig;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return valProposerConfig;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function parseBroadcastValidation(broadcastValidation?: string): routes.beacon.BroadcastValidation | undefined {
|
|
272
|
-
if (broadcastValidation) {
|
|
273
|
-
switch (broadcastValidation) {
|
|
274
|
-
case "gossip":
|
|
275
|
-
case "consensus":
|
|
276
|
-
case "consensus_and_equivocation":
|
|
277
|
-
break;
|
|
278
|
-
default:
|
|
279
|
-
throw new YargsError("Invalid input for broadcastValidation, check help");
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return broadcastValidation as routes.beacon.BroadcastValidation;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function parseWireFormat(args: IValidatorCliArgs, key: keyof IValidatorCliArgs): WireFormat | undefined {
|
|
287
|
-
const wireFormat = args[key];
|
|
288
|
-
|
|
289
|
-
if (wireFormat !== undefined) {
|
|
290
|
-
switch (wireFormat) {
|
|
291
|
-
case WireFormat.json:
|
|
292
|
-
case WireFormat.ssz:
|
|
293
|
-
break;
|
|
294
|
-
default:
|
|
295
|
-
throw new YargsError(`Invalid input for ${key}, must be one of "${WireFormat.json}" or "${WireFormat.ssz}"`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return wireFormat;
|
|
300
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import {Keystore} from "@chainsafe/bls-keystore";
|
|
3
|
-
import {CliCommand} from "@lodestar/utils";
|
|
4
|
-
import {getBeaconConfigFromArgs} from "../../config/beaconParams.js";
|
|
5
|
-
import {GlobalArgs} from "../../options/index.js";
|
|
6
|
-
import {YargsError, getPubkeyHexFromKeystore} from "../../util/index.js";
|
|
7
|
-
import {PersistedKeysBackend} from "./keymanager/persistedKeys.js";
|
|
8
|
-
import {IValidatorCliArgs, validatorOptions} from "./options.js";
|
|
9
|
-
import {getAccountPaths} from "./paths.js";
|
|
10
|
-
import {importKeystoreDefinitionsFromExternalDir, readPassphraseOrPrompt} from "./signers/importExternalKeystores.js";
|
|
11
|
-
|
|
12
|
-
type ValidatorImportArgs = Pick<IValidatorCliArgs, "importKeystores" | "importKeystoresPassword">;
|
|
13
|
-
|
|
14
|
-
const {importKeystores, importKeystoresPassword} = validatorOptions;
|
|
15
|
-
|
|
16
|
-
export const importCmd: CliCommand<ValidatorImportArgs, IValidatorCliArgs & GlobalArgs> = {
|
|
17
|
-
command: "import",
|
|
18
|
-
|
|
19
|
-
describe:
|
|
20
|
-
"Imports one or more EIP-2335 keystores into a Lodestar validator client directory, \
|
|
21
|
-
requesting passwords interactively. The directory flag provides a convenient \
|
|
22
|
-
method for importing a directory of keys generated by the eth2-deposit-cli \
|
|
23
|
-
Ethereum Foundation utility.",
|
|
24
|
-
|
|
25
|
-
examples: [
|
|
26
|
-
{
|
|
27
|
-
command: "validator import --network hoodi --importKeystores $HOME/staking-deposit-cli/validator_keys",
|
|
28
|
-
description: "Import validator keystores generated with the Ethereum Foundation Staking Launchpad",
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
|
|
32
|
-
// Note: re-uses `--importKeystores` and `--importKeystoresPassword` from root validator command options
|
|
33
|
-
|
|
34
|
-
options: {
|
|
35
|
-
importKeystores: {
|
|
36
|
-
...importKeystores,
|
|
37
|
-
requiresArg: true,
|
|
38
|
-
},
|
|
39
|
-
importKeystoresPassword,
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
handler: async (args) => {
|
|
43
|
-
const {network} = getBeaconConfigFromArgs(args);
|
|
44
|
-
|
|
45
|
-
// This command takes: importKeystores, importKeystoresPassword
|
|
46
|
-
//
|
|
47
|
-
// - recursively finds keystores in importKeystores
|
|
48
|
-
// - validates keystores can decrypt
|
|
49
|
-
// - writes them in persisted form - do not lock
|
|
50
|
-
|
|
51
|
-
if (!args.importKeystores) {
|
|
52
|
-
throw new YargsError("Must set importKeystores");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Collect same password for all keystores
|
|
56
|
-
// If importKeystoresPassword is not provided, interactive prompt for it
|
|
57
|
-
|
|
58
|
-
const keystoreDefinitions = importKeystoreDefinitionsFromExternalDir({
|
|
59
|
-
keystoresPath: args.importKeystores,
|
|
60
|
-
password: await readPassphraseOrPrompt(args),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (keystoreDefinitions.length === 0) {
|
|
64
|
-
throw new YargsError("No keystores found");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
console.log(
|
|
68
|
-
`Importing ${keystoreDefinitions.length} keystores:\n ${keystoreDefinitions
|
|
69
|
-
.map((def) => def.keystorePath)
|
|
70
|
-
.join("\n")}`
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const accountPaths = getAccountPaths(args, network);
|
|
74
|
-
const persistedKeystoresBackend = new PersistedKeysBackend(accountPaths);
|
|
75
|
-
let importedCount = 0;
|
|
76
|
-
|
|
77
|
-
for (const {keystorePath, password} of keystoreDefinitions) {
|
|
78
|
-
const keystoreStr = fs.readFileSync(keystorePath, "utf8");
|
|
79
|
-
const keystore = Keystore.parse(keystoreStr);
|
|
80
|
-
const pubkeyHex = getPubkeyHexFromKeystore(keystore);
|
|
81
|
-
|
|
82
|
-
// Check if keystore can decrypt
|
|
83
|
-
if (!(await keystore.verifyPassword(password))) {
|
|
84
|
-
throw Error(`Invalid password for keystore ${keystorePath}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const didImportKey = persistedKeystoresBackend.writeKeystore({
|
|
88
|
-
keystoreStr,
|
|
89
|
-
password,
|
|
90
|
-
// Not used immediately
|
|
91
|
-
lockBeforeWrite: false,
|
|
92
|
-
// Return duplicate status if already found
|
|
93
|
-
persistIfDuplicate: false,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
if (didImportKey) {
|
|
97
|
-
console.log(`Imported keystore ${pubkeyHex} ${keystorePath}`);
|
|
98
|
-
importedCount++;
|
|
99
|
-
} else {
|
|
100
|
-
console.log(`Duplicate keystore ${pubkeyHex} ${keystorePath}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
console.log(`\nSuccessfully imported ${importedCount}/${keystoreDefinitions.length} keystores`);
|
|
105
|
-
|
|
106
|
-
console.log(`
|
|
107
|
-
DO NOT USE THE ORIGINAL KEYSTORES TO VALIDATE WITH
|
|
108
|
-
ANOTHER CLIENT, OR YOU WILL GET SLASHED.
|
|
109
|
-
`);
|
|
110
|
-
},
|
|
111
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import {CliCommand} from "@lodestar/utils";
|
|
2
|
-
import {GlobalArgs} from "../../options/index.js";
|
|
3
|
-
import {blsToExecutionChange} from "./blsToExecutionChange.js";
|
|
4
|
-
import {validatorHandler} from "./handler.js";
|
|
5
|
-
import {importCmd} from "./import.js";
|
|
6
|
-
import {list} from "./list.js";
|
|
7
|
-
import {IValidatorCliArgs, validatorOptions} from "./options.js";
|
|
8
|
-
import {getAccountPaths} from "./paths.js";
|
|
9
|
-
import {slashingProtection} from "./slashingProtection/index.js";
|
|
10
|
-
import {voluntaryExit} from "./voluntaryExit.js";
|
|
11
|
-
|
|
12
|
-
export const validator: CliCommand<IValidatorCliArgs, GlobalArgs> = {
|
|
13
|
-
command: "validator",
|
|
14
|
-
describe: "Run one or multiple validator clients",
|
|
15
|
-
docsFolder: "run/validator-management",
|
|
16
|
-
examples: [
|
|
17
|
-
{
|
|
18
|
-
command: "validator --network hoodi",
|
|
19
|
-
title: "Base `validator` command",
|
|
20
|
-
description:
|
|
21
|
-
"Run one validator client with all the keystores available in the directory" +
|
|
22
|
-
` ${getAccountPaths({dataDir: ".hoodi"}, "hoodi").keystoresDir}`,
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
options: validatorOptions,
|
|
26
|
-
handler: validatorHandler,
|
|
27
|
-
subcommands: [slashingProtection, importCmd, list, voluntaryExit, blsToExecutionChange],
|
|
28
|
-
};
|