@lodestar/flare 1.35.0-dev.f80d2d52da → 1.35.0-dev.fcf8d024ea

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/lib/cli.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import yargs from "yargs";
2
- export declare const yarg: yargs.Argv<{}>;
1
+ import { Argv } from "yargs";
2
+ export declare const yarg: Argv<{}>;
3
3
  /**
4
4
  * Common factory for running the CLI and running integration tests
5
5
  * The CLI must actually be executed in a different script
6
6
  */
7
- export declare function getCli(): yargs.Argv;
7
+ export declare function getCli(): Argv;
8
8
  //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AACA,OAAc,EAAC,IAAI,EAAC,MAAM,OAAO,CAAC;AAgBlC,eAAO,MAAM,IAAI,UAAiE,CAAC;AAEnF;;;GAGG;AACH,wBAAgB,MAAM,IAAI,IAAI,CA6B7B"}
package/lib/cli.js CHANGED
@@ -1,7 +1,7 @@
1
- import { registerCommandToYargs } from "@lodestar/utils";
2
1
  // Must not use `* as yargs`, see https://github.com/yargs/yargs/issues/1131
3
2
  import yargs from "yargs";
4
3
  import { hideBin } from "yargs/helpers";
4
+ import { registerCommandToYargs } from "@lodestar/utils";
5
5
  import { cmds } from "./cmds/index.js";
6
6
  const topBanner = `Beacon chain multi-purpose and debugging tool.
7
7
 
package/lib/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,sBAAsB,EAAC,MAAM,iBAAiB,CAAC;AACvD,4EAA4E;AAC5E,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AACtC,OAAO,EAAC,IAAI,EAAC,MAAM,iBAAiB,CAAC;AAErC,MAAM,SAAS,GAAG;;;;;;iCAMe,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;AAC5D,MAAM,YAAY,GAAG;;0CAEqB,CAAC;AAE3C,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,CAAE,OAAwC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAEnF;;;GAGG;AACH,MAAM,UAAU,MAAM;IACpB,MAAM,QAAQ,GAAG,IAAI;SAClB,GAAG,CAAC,OAAO,CAAC;SACZ,mBAAmB,CAAC;QACnB,0DAA0D;QAC1D,2DAA2D;QAC3D,cAAc,EAAE,KAAK;KACtB,CAAC;QACF,sFAAsF;SACrF,UAAU,CAAC,EAAE,CAAC;SACd,aAAa,CAAC,CAAC,CAAC;QACjB,+CAA+C;SAC9C,cAAc,CAAC,KAAK,CAAC;SACrB,KAAK,CAAC,SAAS,CAAC;SAChB,QAAQ,CAAC,YAAY,CAAC;SACtB,OAAO,CAAC,SAAS,CAAC;SAClB,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC;SAClB,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC;SACrB,iBAAiB,EAAE,CAAC;IAEvB,+BAA+B;IAC/B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,sBAAsB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,+CAA+C;IAC/C,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,CAAC;IAEtC,OAAO,QAAQ,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,OAAO,KAAa,MAAM,OAAO,CAAC;AAClC,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AACtC,OAAO,EAAC,sBAAsB,EAAC,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAC,IAAI,EAAC,MAAM,iBAAiB,CAAC;AAErC,MAAM,SAAS,GAAG;;;;;;iCAMe,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;AAC5D,MAAM,YAAY,GAAG;;0CAEqB,CAAC;AAE3C,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,CAAE,OAAwC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAEnF;;;GAGG;AACH,MAAM,UAAU,MAAM;IACpB,MAAM,QAAQ,GAAG,IAAI;SAClB,GAAG,CAAC,OAAO,CAAC;SACZ,mBAAmB,CAAC;QACnB,0DAA0D;QAC1D,2DAA2D;QAC3D,cAAc,EAAE,KAAK;KACtB,CAAC;QACF,sFAAsF;SACrF,UAAU,CAAC,EAAE,CAAC;SACd,aAAa,CAAC,CAAC,CAAC;QACjB,+CAA+C;SAC9C,cAAc,CAAC,KAAK,CAAC;SACrB,KAAK,CAAC,SAAS,CAAC;SAChB,QAAQ,CAAC,YAAY,CAAC;SACtB,OAAO,CAAC,SAAS,CAAC;SAClB,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC;SAClB,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC;SACrB,iBAAiB,EAAE,CAAC;IAEvB,+BAA+B;IAC/B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,sBAAsB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,+CAA+C;IAC/C,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,CAAC;IAEtC,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cmds/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAI3C,eAAO,MAAM,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAGhG,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfSlashAttester.d.ts","sourceRoot":"","sources":["../../src/cmds/selfSlashAttester.ts"],"names":[],"mappings":"AAOA,OAAO,EAAC,UAAU,EAAc,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAC,cAAc,EAAsC,MAAM,6BAA6B,CAAC;AAEhG,KAAK,aAAa,GAAG,cAAc,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,UAAU,CAAC,aAAa,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,CA4BnF,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA0FjF"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfSlashProposer.d.ts","sourceRoot":"","sources":["../../src/cmds/selfSlashProposer.ts"],"names":[],"mappings":"AAOA,OAAO,EAAC,UAAU,EAAc,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAC,cAAc,EAAsC,MAAM,6BAA6B,CAAC;AAEhG,KAAK,aAAa,GAAG,cAAc,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,UAAU,CAAC,aAAa,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,CA4BnF,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoFjF"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,gCAAgC,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deriveSecretKeys.d.ts","sourceRoot":"","sources":["../../src/util/deriveSecretKeys.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE1C,OAAO,EAAC,iBAAiB,EAAC,MAAM,iBAAiB,CAAC;AAIlD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,iBAAiB,CAAC,cAAc,CAa/D,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,EAAE,CAoBlE"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/util/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,UAAW,SAAQ,KAAK;CAAG"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/util/format.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBlD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodestar/flare",
3
- "version": "1.35.0-dev.f80d2d52da",
3
+ "version": "1.35.0-dev.fcf8d024ea",
4
4
  "description": "Beacon chain debugging tool",
5
5
  "author": "ChainSafe Systems",
6
6
  "license": "Apache-2.0",
@@ -11,6 +11,7 @@
11
11
  "type": "module",
12
12
  "exports": {
13
13
  ".": {
14
+ "bun": "./src/index.ts",
14
15
  "import": "./lib/index.js"
15
16
  }
16
17
  },
@@ -25,11 +26,9 @@
25
26
  },
26
27
  "types": "lib/index.d.ts",
27
28
  "files": [
28
- "lib/**/*.js",
29
- "lib/**/*.js.map",
30
- "lib/**/*.d.ts",
31
- "*.d.ts",
32
- "*.js"
29
+ "src",
30
+ "lib",
31
+ "!**/*.tsbuildinfo"
33
32
  ],
34
33
  "bin": {
35
34
  "flare": "lib/index.js"
@@ -60,17 +59,17 @@
60
59
  "dependencies": {
61
60
  "@chainsafe/bls-keygen": "^0.4.0",
62
61
  "@chainsafe/blst": "^2.2.0",
63
- "@lodestar/api": "1.35.0-dev.f80d2d52da",
64
- "@lodestar/config": "1.35.0-dev.f80d2d52da",
65
- "@lodestar/params": "1.35.0-dev.f80d2d52da",
66
- "@lodestar/state-transition": "1.35.0-dev.f80d2d52da",
67
- "@lodestar/types": "1.35.0-dev.f80d2d52da",
68
- "@lodestar/utils": "1.35.0-dev.f80d2d52da",
62
+ "@lodestar/api": "1.35.0-dev.fcf8d024ea",
63
+ "@lodestar/config": "1.35.0-dev.fcf8d024ea",
64
+ "@lodestar/params": "1.35.0-dev.fcf8d024ea",
65
+ "@lodestar/state-transition": "1.35.0-dev.fcf8d024ea",
66
+ "@lodestar/types": "1.35.0-dev.fcf8d024ea",
67
+ "@lodestar/utils": "1.35.0-dev.fcf8d024ea",
69
68
  "source-map-support": "^0.5.21",
70
69
  "yargs": "^17.7.1"
71
70
  },
72
71
  "devDependencies": {
73
72
  "@types/yargs": "^17.0.24"
74
73
  },
75
- "gitHead": "16b0aa696115a011948479fd965d742e131dfc83"
74
+ "gitHead": "d72abda1a1607ce062febfcfc4528b9e9848c288"
76
75
  }
package/src/cli.ts ADDED
@@ -0,0 +1,53 @@
1
+ // Must not use `* as yargs`, see https://github.com/yargs/yargs/issues/1131
2
+ import yargs, {Argv} from "yargs";
3
+ import {hideBin} from "yargs/helpers";
4
+ import {registerCommandToYargs} from "@lodestar/utils";
5
+ import {cmds} from "./cmds/index.js";
6
+
7
+ const topBanner = `Beacon chain multi-purpose and debugging tool.
8
+
9
+ Flare is a sudden brief burst of bright flame or light.
10
+ In the wrong hands, can lead people astray.
11
+ Use with care.
12
+
13
+ * by ChainSafe Systems, 2018-${new Date().getFullYear()}`;
14
+ const bottomBanner = `
15
+ ✍️ Give feedback and report issues on GitHub:
16
+ * https://github.com/ChainSafe/lodestar`;
17
+
18
+ export const yarg = yargs((hideBin as (args: string[]) => string[])(process.argv));
19
+
20
+ /**
21
+ * Common factory for running the CLI and running integration tests
22
+ * The CLI must actually be executed in a different script
23
+ */
24
+ export function getCli(): Argv {
25
+ const lodestar = yarg
26
+ .env("FLARE")
27
+ .parserConfiguration({
28
+ // As of yargs v16.1.0 dot-notation breaks strictOptions()
29
+ // Manually processing options is typesafe tho more verbose
30
+ "dot-notation": false,
31
+ })
32
+ // blank scriptName so that help text doesn't display the cli name before each command
33
+ .scriptName("")
34
+ .demandCommand(1)
35
+ // Control show help behaviour below on .fail()
36
+ .showHelpOnFail(false)
37
+ .usage(topBanner)
38
+ .epilogue(bottomBanner)
39
+ .version(topBanner)
40
+ .alias("h", "help")
41
+ .alias("v", "version")
42
+ .recommendCommands();
43
+
44
+ // yargs.command and all ./cmds
45
+ for (const cmd of cmds) {
46
+ registerCommandToYargs(lodestar, cmd);
47
+ }
48
+
49
+ // throw an error if we see an unrecognized cmd
50
+ lodestar.recommendCommands().strict();
51
+
52
+ return lodestar;
53
+ }
@@ -0,0 +1,8 @@
1
+ import {CliCommand} from "@lodestar/utils";
2
+ import {selfSlashAttester} from "./selfSlashAttester.js";
3
+ import {selfSlashProposer} from "./selfSlashProposer.js";
4
+
5
+ export const cmds: Required<CliCommand<Record<never, never>, Record<never, never>>>["subcommands"] = [
6
+ selfSlashProposer,
7
+ selfSlashAttester,
8
+ ];
@@ -0,0 +1,150 @@
1
+ import {SecretKey, aggregateSignatures} from "@chainsafe/blst";
2
+ import {getClient} from "@lodestar/api";
3
+ import {BeaconConfig, createBeaconConfig} from "@lodestar/config";
4
+ import {config as chainConfig} from "@lodestar/config/default";
5
+ import {DOMAIN_BEACON_ATTESTER, MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params";
6
+ import {computeSigningRoot} from "@lodestar/state-transition";
7
+ import {AttesterSlashing, phase0, ssz} from "@lodestar/types";
8
+ import {CliCommand, toPubkeyHex} from "@lodestar/utils";
9
+ import {SecretKeysArgs, deriveSecretKeys, secretKeysOptions} from "../util/deriveSecretKeys.js";
10
+
11
+ type SelfSlashArgs = SecretKeysArgs & {
12
+ server: string;
13
+ slot: string;
14
+ batchSize: string;
15
+ };
16
+
17
+ export const selfSlashAttester: CliCommand<SelfSlashArgs, Record<never, never>, void> = {
18
+ command: "self-slash-attester",
19
+ describe: "Self slash validators of a provided mnemonic with AttesterSlashing",
20
+ examples: [
21
+ {
22
+ command: "self-slash-proposer --network holesky",
23
+ description: "Self slash validators of a provided mnemonic",
24
+ },
25
+ ],
26
+ options: {
27
+ ...secretKeysOptions,
28
+ server: {
29
+ description: "Address to connect to BeaconNode",
30
+ default: "http://127.0.0.1:9596",
31
+ type: "string",
32
+ },
33
+ slot: {
34
+ description: "AttesterSlashing data slot",
35
+ default: "0",
36
+ type: "string",
37
+ },
38
+ batchSize: {
39
+ description: "Add batchSize validators in each AttesterSlashing. Must be < MAX_VALIDATORS_PER_COMMITTEE",
40
+ default: "10",
41
+ type: "string",
42
+ },
43
+ },
44
+ handler: selfSlashAttesterHandler,
45
+ };
46
+
47
+ export async function selfSlashAttesterHandler(args: SelfSlashArgs): Promise<void> {
48
+ const sksAll = deriveSecretKeys(args);
49
+
50
+ const slot = BigInt(args.slot); // Throws if not valid
51
+ const batchSize = parseInt(args.batchSize);
52
+
53
+ if (Number.isNaN(batchSize)) throw Error(`Invalid arg batchSize ${args.batchSize}`);
54
+ if (batchSize <= 0) throw Error(`batchSize must be > 0: ${batchSize}`);
55
+ if (batchSize > MAX_VALIDATORS_PER_COMMITTEE) throw Error("batchSize must be < MAX_VALIDATORS_PER_COMMITTEE");
56
+
57
+ // TODO: Ask the user to confirm the range and slash action
58
+
59
+ const client = getClient({baseUrl: args.server}, {config: chainConfig});
60
+
61
+ // Get genesis data to perform correct signatures
62
+ const {genesisValidatorsRoot} = (await client.beacon.getGenesis()).value();
63
+
64
+ const config = createBeaconConfig(chainConfig, genesisValidatorsRoot);
65
+
66
+ // TODO: Allow to customize the ProposerSlashing payloads
67
+
68
+ const rootA = Buffer.alloc(32, 0xaa);
69
+ const rootB = Buffer.alloc(32, 0xbb);
70
+
71
+ // To log progress
72
+ let successCount = 0;
73
+ const totalCount = sksAll.length;
74
+
75
+ for (let n = 0; n < sksAll.length; n += batchSize) {
76
+ const sks = sksAll.slice(n, n + batchSize);
77
+
78
+ // Retrieve the status all all validators in range at once
79
+ const pksHex = sks.map((sk) => sk.toPublicKey().toHex());
80
+ const validators = (await client.beacon.postStateValidators({stateId: "head", validatorIds: pksHex})).value();
81
+
82
+ // All validators in the batch will be part of the same AttesterSlashing
83
+ const attestingIndices = validators.map((v) => v.index);
84
+
85
+ // Submit all ProposerSlashing for range at once
86
+
87
+ // Ensure sorted response
88
+ for (let i = 0; i < pksHex.length; i++) {
89
+ const {index, status, validator} = validators[i];
90
+ const pkHex = pksHex[i];
91
+ const validatorPkHex = toPubkeyHex(validator.pubkey);
92
+ if (validatorPkHex !== pkHex) {
93
+ throw Error(`Beacon node did not return same validator pubkey: ${validatorPkHex} != ${pkHex}`);
94
+ }
95
+
96
+ if (status === "active_slashed" || status === "exited_slashed") {
97
+ console.log(`Warning: validator index ${index} is already slashed`);
98
+ }
99
+ }
100
+
101
+ // Trigers a double vote, same target epoch different data (beaconBlockRoot)
102
+ // TODO: Allow to create double-votes
103
+ const data1: phase0.AttestationDataBigint = {
104
+ slot,
105
+ index: BigInt(0),
106
+ beaconBlockRoot: rootA,
107
+ source: {epoch: BigInt(0), root: rootA},
108
+ target: {epoch: BigInt(0), root: rootB},
109
+ };
110
+ const data2: phase0.AttestationDataBigint = {
111
+ slot,
112
+ index: BigInt(0),
113
+ beaconBlockRoot: rootB,
114
+ source: {epoch: BigInt(0), root: rootA},
115
+ target: {epoch: BigInt(0), root: rootB},
116
+ };
117
+
118
+ const attesterSlashing: AttesterSlashing = {
119
+ attestation1: {
120
+ attestingIndices,
121
+ data: data1,
122
+ signature: signAttestationDataBigint(config, sks, data1),
123
+ },
124
+ attestation2: {
125
+ attestingIndices,
126
+ data: data2,
127
+ signature: signAttestationDataBigint(config, sks, data2),
128
+ },
129
+ };
130
+
131
+ (await client.beacon.submitPoolAttesterSlashingsV2({attesterSlashing})).assertOk();
132
+
133
+ successCount += attestingIndices.length;
134
+ const indexesStr = attestingIndices.join(",");
135
+ console.log(`Submitted self AttesterSlashing for validators ${indexesStr} - ${successCount}/${totalCount}`);
136
+ }
137
+ }
138
+
139
+ function signAttestationDataBigint(
140
+ config: BeaconConfig,
141
+ sks: SecretKey[],
142
+ data: phase0.AttestationDataBigint
143
+ ): Uint8Array {
144
+ const slot = Number(data.slot as bigint);
145
+ const proposerDomain = config.getDomain(slot, DOMAIN_BEACON_ATTESTER);
146
+ const signingRoot = computeSigningRoot(ssz.phase0.AttestationDataBigint, data, proposerDomain);
147
+
148
+ const sigs = sks.map((sk) => sk.sign(signingRoot));
149
+ return aggregateSignatures(sigs).toBytes();
150
+ }
@@ -0,0 +1,138 @@
1
+ import {SecretKey} from "@chainsafe/blst";
2
+ import {getClient} from "@lodestar/api";
3
+ import {BeaconConfig, createBeaconConfig} from "@lodestar/config";
4
+ import {config as chainConfig} from "@lodestar/config/default";
5
+ import {DOMAIN_BEACON_PROPOSER} from "@lodestar/params";
6
+ import {computeSigningRoot} from "@lodestar/state-transition";
7
+ import {phase0, ssz} from "@lodestar/types";
8
+ import {CliCommand, toPubkeyHex} from "@lodestar/utils";
9
+ import {SecretKeysArgs, deriveSecretKeys, secretKeysOptions} from "../util/deriveSecretKeys.js";
10
+
11
+ type SelfSlashArgs = SecretKeysArgs & {
12
+ server: string;
13
+ slot: string;
14
+ batchSize: string;
15
+ };
16
+
17
+ export const selfSlashProposer: CliCommand<SelfSlashArgs, Record<never, never>, void> = {
18
+ command: "self-slash-proposer",
19
+ describe: "Self slash validators of a provided mnemonic with ProposerSlashing",
20
+ examples: [
21
+ {
22
+ command: "self-slash-proposer --network holesky",
23
+ description: "Self slash validators of a provided mnemonic",
24
+ },
25
+ ],
26
+ options: {
27
+ ...secretKeysOptions,
28
+ server: {
29
+ description: "Address to connect to BeaconNode",
30
+ default: "http://127.0.0.1:9596",
31
+ type: "string",
32
+ },
33
+ slot: {
34
+ description: "ProposerSlashing headers slot",
35
+ default: "0",
36
+ type: "string",
37
+ },
38
+ batchSize: {
39
+ description: "Send in batches of size batchSize",
40
+ default: "10",
41
+ type: "string",
42
+ },
43
+ },
44
+ handler: selfSlashProposerHandler,
45
+ };
46
+
47
+ export async function selfSlashProposerHandler(args: SelfSlashArgs): Promise<void> {
48
+ const sksAll = deriveSecretKeys(args);
49
+
50
+ const slot = BigInt(args.slot); // Throws if not valid
51
+ const batchSize = parseInt(args.batchSize);
52
+
53
+ if (Number.isNaN(batchSize)) throw Error(`Invalid arg batchSize ${args.batchSize}`);
54
+ if (batchSize <= 0) throw Error(`batchSize must be > 0: ${batchSize}`);
55
+
56
+ // TODO: Ask the user to confirm the range and slash action
57
+
58
+ const client = getClient({baseUrl: args.server}, {config: chainConfig});
59
+
60
+ // Get genesis data to perform correct signatures
61
+ const {genesisValidatorsRoot} = (await client.beacon.getGenesis()).value();
62
+ const config = createBeaconConfig(chainConfig, genesisValidatorsRoot);
63
+
64
+ // TODO: Allow to customize the ProposerSlashing payloads
65
+
66
+ const rootA = Buffer.alloc(32, 0xaa);
67
+ const rootB = Buffer.alloc(32, 0xbb);
68
+
69
+ // To log progress
70
+ let successCount = 0;
71
+ const totalCount = sksAll.length;
72
+
73
+ for (let n = 0; n < sksAll.length; n += batchSize) {
74
+ const sks = sksAll.slice(n, n + batchSize);
75
+
76
+ // Retrieve the status all all validators in range at once
77
+ const pksHex = sks.map((sk) => sk.toPublicKey().toHex());
78
+ const validators = (await client.beacon.postStateValidators({stateId: "head", validatorIds: pksHex})).value();
79
+
80
+ // Submit all ProposerSlashing for range at once
81
+ await Promise.all(
82
+ pksHex.map(async (pkHex, i) => {
83
+ const sk = sks[i];
84
+ const {index, status, validator} = validators[i];
85
+
86
+ try {
87
+ const validatorPkHex = toPubkeyHex(validator.pubkey);
88
+ if (validatorPkHex !== pkHex) {
89
+ throw Error(`Beacon node did not return same validator pubkey: ${validatorPkHex} != ${pkHex}`);
90
+ }
91
+
92
+ if (status === "active_slashed" || status === "exited_slashed") {
93
+ console.log(`Warning: validator index ${index} is already slashed`);
94
+ }
95
+
96
+ const header1: phase0.BeaconBlockHeaderBigint = {
97
+ slot,
98
+ proposerIndex: index,
99
+ parentRoot: rootA,
100
+ stateRoot: rootA,
101
+ bodyRoot: rootA,
102
+ };
103
+ const header2: phase0.BeaconBlockHeaderBigint = {
104
+ slot,
105
+ proposerIndex: index,
106
+ parentRoot: rootB,
107
+ stateRoot: rootB,
108
+ bodyRoot: rootB,
109
+ };
110
+
111
+ const proposerSlashing: phase0.ProposerSlashing = {
112
+ signedHeader1: {
113
+ message: header1,
114
+ signature: signHeaderBigint(config, sk, header1),
115
+ },
116
+ signedHeader2: {
117
+ message: header2,
118
+ signature: signHeaderBigint(config, sk, header2),
119
+ },
120
+ };
121
+
122
+ (await client.beacon.submitPoolProposerSlashings({proposerSlashing})).assertOk();
123
+
124
+ console.log(`Submitted self ProposerSlashing for validator ${index} - ${++successCount}/${totalCount}`);
125
+ } catch (e) {
126
+ (e as Error).message = `Error slashing validator ${index}: ${(e as Error).message}`;
127
+ }
128
+ })
129
+ );
130
+ }
131
+ }
132
+
133
+ function signHeaderBigint(config: BeaconConfig, sk: SecretKey, header: phase0.BeaconBlockHeaderBigint): Uint8Array {
134
+ const slot = Number(header.slot as bigint);
135
+ const proposerDomain = config.getDomain(slot, DOMAIN_BEACON_PROPOSER);
136
+ const signingRoot = computeSigningRoot(ssz.phase0.BeaconBlockHeaderBigint, header, proposerDomain);
137
+ return sk.sign(signingRoot).toBytes();
138
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {getCli, yarg} from "./cli.js";
4
+ import {YargsError} from "./util/errors.js";
5
+ import "source-map-support/register.js";
6
+
7
+ const flare = getCli();
8
+
9
+ void flare
10
+ .fail((msg, err) => {
11
+ if (msg?.includes("Not enough non-option arguments")) {
12
+ // Show command help message when no command is provided
13
+ yarg.showHelp();
14
+ console.log("\n");
15
+ }
16
+
17
+ const errorMessage =
18
+ err !== undefined ? (err instanceof YargsError ? err.message : err.stack) : msg || "Unknown error";
19
+
20
+ console.error(` ✖ ${errorMessage}\n`);
21
+ process.exit(1);
22
+ })
23
+
24
+ // Execute CLI
25
+ .parse();
@@ -0,0 +1,49 @@
1
+ import {deriveEth2ValidatorKeys, deriveKeyFromMnemonic} from "@chainsafe/bls-keygen";
2
+ import {SecretKey} from "@chainsafe/blst";
3
+ import {interopSecretKey} from "@lodestar/state-transition";
4
+ import {CliCommandOptions} from "@lodestar/utils";
5
+ import {YargsError} from "./errors.js";
6
+ import {parseRange} from "./format.js";
7
+
8
+ export type SecretKeysArgs = {
9
+ mnemonic?: string;
10
+ indexes?: string;
11
+ interopIndexes?: string;
12
+ };
13
+
14
+ export const secretKeysOptions: CliCommandOptions<SecretKeysArgs> = {
15
+ mnemonic: {
16
+ description: "Mnemonic to derive private keys from",
17
+ type: "string",
18
+ },
19
+ indexes: {
20
+ description: "Range of indexes to select, in inclusive range with notation '0..7'",
21
+ type: "string",
22
+ },
23
+ interopIndexes: {
24
+ description: "Range of interop indexes to select, in inclusive range with notation '0..7'",
25
+ type: "string",
26
+ },
27
+ };
28
+
29
+ export function deriveSecretKeys(args: SecretKeysArgs): SecretKey[] {
30
+ if (args.interopIndexes) {
31
+ const indexes = parseRange(args.interopIndexes);
32
+ return indexes.map((index) => interopSecretKey(index));
33
+ }
34
+
35
+ if (args.indexes || args.mnemonic) {
36
+ if (!args.mnemonic) throw new YargsError("arg mnemonic is required");
37
+ if (!args.indexes) throw new YargsError("arg indexes is required");
38
+
39
+ const masterSK = deriveKeyFromMnemonic(args.mnemonic);
40
+ const indexes = parseRange(args.indexes);
41
+
42
+ return indexes.map((index) => {
43
+ const {signing} = deriveEth2ValidatorKeys(masterSK, index);
44
+ return SecretKey.fromBytes(signing);
45
+ });
46
+ }
47
+
48
+ throw new YargsError("Must set arg interopIndexes or mnemonic");
49
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Expected error that shouldn't print a stack trace
3
+ */
4
+ export class YargsError extends Error {}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Parse string inclusive range: `0..32`, into an array of all values in that range
3
+ */
4
+ export function parseRange(range: string): number[] {
5
+ if (!range.includes("..")) {
6
+ throw Error(`Invalid range '${range}', must include '..'`);
7
+ }
8
+
9
+ const [from, to] = range.split("..").map((n) => parseInt(n));
10
+
11
+ if (Number.isNaN(from)) throw Error(`Invalid range from isNaN '${range}'`);
12
+ if (Number.isNaN(to)) throw Error(`Invalid range to isNaN '${range}'`);
13
+ if (from > to) throw Error(`Invalid range from > to '${range}'`);
14
+
15
+ const arr: number[] = [];
16
+ for (let i = from; i <= to; i++) {
17
+ arr.push(i);
18
+ }
19
+
20
+ return arr;
21
+ }