@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 +3 -3
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +1 -1
- package/lib/cli.js.map +1 -1
- package/lib/cmds/index.d.ts.map +1 -0
- package/lib/cmds/selfSlashAttester.d.ts.map +1 -0
- package/lib/cmds/selfSlashProposer.d.ts.map +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/util/deriveSecretKeys.d.ts.map +1 -0
- package/lib/util/errors.d.ts.map +1 -0
- package/lib/util/format.d.ts.map +1 -0
- package/package.json +12 -13
- package/src/cli.ts +53 -0
- package/src/cmds/index.ts +8 -0
- package/src/cmds/selfSlashAttester.ts +150 -0
- package/src/cmds/selfSlashProposer.ts +138 -0
- package/src/index.ts +25 -0
- package/src/util/deriveSecretKeys.ts +49 -0
- package/src/util/errors.ts +4 -0
- package/src/util/format.ts +21 -0
package/lib/cli.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
export declare const yarg:
|
|
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():
|
|
7
|
+
export declare function getCli(): Argv;
|
|
8
8
|
//# sourceMappingURL=cli.d.ts.map
|
package/lib/cli.d.ts.map
ADDED
|
@@ -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,
|
|
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.
|
|
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
|
-
"
|
|
29
|
-
"lib
|
|
30
|
-
"
|
|
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.
|
|
64
|
-
"@lodestar/config": "1.35.0-dev.
|
|
65
|
-
"@lodestar/params": "1.35.0-dev.
|
|
66
|
-
"@lodestar/state-transition": "1.35.0-dev.
|
|
67
|
-
"@lodestar/types": "1.35.0-dev.
|
|
68
|
-
"@lodestar/utils": "1.35.0-dev.
|
|
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": "
|
|
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,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
|
+
}
|