diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 23285bee..0a12d5e5 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -50,7 +50,7 @@ jobs: if: ${{ matrix.os == 'macos' }} uses: actions/setup-python@v4 with: - python-version: "3.11.3" + python-version: "3.11.4" - name: Set up Node uses: actions/setup-node@v3 with: diff --git a/.gitignore b/.gitignore index 966ce72e..8002bf87 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,10 @@ dist/ build/ src/scripts/__pycache__/ stakingdeposit_proxy.spec +eth2deposit_proxy.spec REQUIREMENT_PACKAGES_PATH/ .build/ # Debugging keys/ +.DS_Store diff --git a/README.md b/README.md index f194f6d4..fd5c9c41 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Execute all those commands in your terminal to setup your dev environment. You m ```console /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/wagyu/.zprofile +echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/$USER/.zprofile eval "$(/opt/homebrew/bin/brew shellenv)" git --version diff --git a/package.json b/package.json index e1760222..a6f74cb6 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "extraFiles": [ "build/bin/*", { - "from": ".build/${arch}/eth2deposit_proxy", - "to": "build/bin/eth2deposit_proxy" + "from": ".build/${arch}/stakingdeposit_proxy", + "to": "build/bin/stakingdeposit_proxy" }, "build/word_lists/*", "static/icon.png", diff --git a/src/electron/Eth2Deposit.ts b/src/electron/Eth2Deposit.ts index fc31ade1..a0c0c127 100644 --- a/src/electron/Eth2Deposit.ts +++ b/src/electron/Eth2Deposit.ts @@ -1,17 +1,17 @@ // Eth2Deposit.ts /** - * This Eth2Deposit module exposes the different functions exported by the eth2deposit_proxy + * This Eth2Deposit module exposes the different functions exported by the stakingdeposit_proxy * application to be used easily with our typescript code. * - * The eth2deposit_proxy application can be called in 3 different ways: + * The stakingdeposit_proxy application can be called in 3 different ways: * 1. From a bundled application, when we do release with electron-builder. This bundled - * application will always include a single file application (SFE) version of eth2deposit_proxy. + * application will always include a single file application (SFE) version of stakingdeposit_proxy. * 2. Using a single file application (SFE) bundled with pyinstaller in an environment where the * running application is not bundled. * 3. Using the Python 3 version installed on the current machine and the version available * in the current environment. * - * When we want to call the eth2deposit_proxy application, it will detect which way can be called + * When we want to call the stakingdeposit_proxy application, it will detect which way can be called * in order and use the first one available. * * @module @@ -36,7 +36,7 @@ const execFileProm = promisify(execFile); const ETH2_DEPOSIT_DIR_NAME = "tools-key-gen-cli"; /** - * Paths needed to call the eth2deposit_proxy application using the Python 3 version installed on + * Paths needed to call the stakingdeposit_proxy application using the Python 3 version installed on * the current machine. */ const ETH2_DEPOSIT_CLI_PATH = path.join( @@ -54,21 +54,21 @@ const WORD_LIST_PATH = path.join( "word_lists" ); const REQUIREMENT_PACKAGES_PATH = path.join("dist", "packages"); -const ETH2DEPOSIT_PROXY_PATH = path.join(SCRIPTS_PATH, "eth2deposit_proxy.py"); +const STAKINGDEPOSIT_PROXY_PATH = path.join(SCRIPTS_PATH, "stakingdeposit_proxy.py"); /** - * Paths needed to call the eth2deposit_proxy application using a single file application (SFE) + * Paths needed to call the stakingdeposit_proxy application using a single file application (SFE) * bundled with pyinstaller. */ const SFE_PATH = path.join( "build", "bin", - "eth2deposit_proxy" + (process.platform == "win32" ? ".exe" : "") + "stakingdeposit_proxy" + (process.platform == "win32" ? ".exe" : "") ); const DIST_WORD_LIST_PATH = path.join(cwd(), "build", "word_lists"); /** - * Paths needed to call the eth2deposit_proxy application from a bundled application. + * Paths needed to call the stakingdeposit_proxy application from a bundled application. */ const BUNDLED_SFE_PATH = process.platform === "darwin" @@ -77,14 +77,14 @@ const BUNDLED_SFE_PATH = "..", "build", "bin", - "eth2deposit_proxy/eth2deposit_proxy" + "stakingdeposit_proxy/stakingdeposit_proxy" ) : path.join( process.resourcesPath, "..", "build", "bin", - "eth2deposit_proxy" + (process.platform === "win32" ? ".exe" : "") + "stakingdeposit_proxy" + (process.platform === "win32" ? ".exe" : "") ); const BUNDLED_DIST_WORD_LIST_PATH = path.join( @@ -97,12 +97,14 @@ const BUNDLED_DIST_WORD_LIST_PATH = path.join( const CREATE_MNEMONIC_SUBCOMMAND = "create_mnemonic"; const GENERATE_KEYS_SUBCOMMAND = "generate_keys"; const VALIDATE_MNEMONIC_SUBCOMMAND = "validate_mnemonic"; +const VALIDATE_BLS_CREDENTIALS_SUBCOMMAND = "validate_bls_credentials"; +const VALIDATE_BLS_CHANGE_SUBCOMMAND = "bls_change"; const PYTHON_EXE = process.platform == "win32" ? "python" : "python3"; const PATH_DELIM = process.platform == "win32" ? ";" : ":"; /** - * Install the required Python packages needed to call the eth2deposit_proxy application using the + * Install the required Python packages needed to call the stakingdeposit_proxy application using the * Python 3 version installed on the current machine. * * @returns Returns a Promise that includes a true value if the required Python packages @@ -148,7 +150,7 @@ const getPythonPath = async (): Promise => { }; /** - * Create a new mnemonic by calling the create_mnemonic function from the eth2deposit_proxy + * Create a new mnemonic by calling the create_mnemonic function from the stakingdeposit_proxy * application. * * @param language The mnemonic language. Possible values are `chinese_simplified`, @@ -188,7 +190,7 @@ const createMnemonic = async (language: string): Promise => { executable = PYTHON_EXE; args = [ - ETH2DEPOSIT_PROXY_PATH, + STAKINGDEPOSIT_PROXY_PATH, CREATE_MNEMONIC_SUBCOMMAND, WORD_LIST_PATH, "--language", @@ -204,7 +206,7 @@ const createMnemonic = async (language: string): Promise => { }; /** - * Generate validator keys by calling the generate_keys function from the eth2deposit_proxy + * Generate validator keys by calling the generate_keys function from the stakingdeposit_proxy * application. * * @param mnemonic The mnemonic to be used as the seed for generating the keys. @@ -281,7 +283,7 @@ const generateKeys = async ( env.PYTHONPATH = await getPythonPath(); executable = PYTHON_EXE; - args = [ETH2DEPOSIT_PROXY_PATH, GENERATE_KEYS_SUBCOMMAND]; + args = [STAKINGDEPOSIT_PROXY_PATH, GENERATE_KEYS_SUBCOMMAND]; if (eth1_withdrawal_address != "") { args = args.concat([ "--eth1_withdrawal_address", @@ -305,7 +307,7 @@ const generateKeys = async ( /** * Validate a mnemonic using the eth2-deposit-cli logic by calling the validate_mnemonic function - * from the eth2deposit_proxy application. + * from the stakingdeposit_proxy application. * * @param mnemonic The mnemonic to be validated. * @@ -336,7 +338,7 @@ const validateMnemonic = async (mnemonic: string): Promise => { executable = PYTHON_EXE; args = [ - ETH2DEPOSIT_PROXY_PATH, + STAKINGDEPOSIT_PROXY_PATH, VALIDATE_MNEMONIC_SUBCOMMAND, WORD_LIST_PATH, mnemonic, @@ -346,4 +348,110 @@ const validateMnemonic = async (mnemonic: string): Promise => { await execFileProm(executable, args, { env: env }); }; -export { createMnemonic, generateKeys, validateMnemonic }; +/** + * Validate BLS credentials by calling the validate_bls_credentials function + * from the stakingdeposit_proxy application. + * + * @param chain The network setting for the signing domain. Possible values are `mainnet`, + * `goerli`, `zhejiang`. + * @param mnemonic The mnemonic from which the BLS credentials are derived. + * @param index The index of the first validator's keys. + * @param withdrawal_credentials A list of the old BLS withdrawal credentials of the given validator(s), comma separated. + * + * @returns Returns a Promise that will resolve when the validation is done. + */ +const validateBLSCredentials = async ( + chain: string, + mnemonic: string, + index: number, + withdrawal_credentials: string +): Promise => { + + let executable:string = ""; + let args:string[] = []; + let env = process.env; + + if (await doesFileExist(BUNDLED_SFE_PATH)) { + executable = BUNDLED_SFE_PATH; + args = [VALIDATE_BLS_CREDENTIALS_SUBCOMMAND, chain.toLowerCase(), mnemonic, index.toString(), withdrawal_credentials]; + } else if (await doesFileExist(SFE_PATH)) { + executable = SFE_PATH; + args = [VALIDATE_BLS_CREDENTIALS_SUBCOMMAND, chain.toLowerCase(), mnemonic, index.toString(), withdrawal_credentials]; + } else { + if(!await requireDepositPackages()) { + throw new Error("Failed to validate BLS credentials, don't have the required packages."); + } + env.PYTHONPATH = await getPythonPath(); + + executable = PYTHON_EXE; + args = [STAKINGDEPOSIT_PROXY_PATH, VALIDATE_BLS_CREDENTIALS_SUBCOMMAND, chain.toLowerCase(), mnemonic, index.toString(), withdrawal_credentials]; + } + + await execFileProm(executable, args, {env: env}); +} + +/** + * Generate BTEC file by calling the bls_change function from the stakingdeposit_proxy + * application. + * + * @param folder The folder path for the resulting BTEC file. + * @param chain The network setting for the signing domain. Possible values are `mainnet`, + * `goerli`, `zhejiang`. + * @param mnemonic The mnemonic to be used as the seed for generating the BTEC. + * @param index The index of the first validator's keys. + * @param indices The validator index number(s) as identified on the beacon chain (comma seperated). + * @param withdrawal_credentials A list of the old BLS withdrawal credentials of the given validator(s), comma separated. + * @param execution_address The withdrawal address. + * + * @returns Returns a Promise that will resolve when the generation is done. + */ +const generateBLSChange = async ( + folder: string, + chain: string, + mnemonic: string, + index: number, + indices: string, + withdrawal_credentials: string, + execution_address: string + +): Promise => { + +let executable:string = ""; +let args:string[] = []; +let env = process.env; + +if (await doesFileExist(BUNDLED_SFE_PATH)) { + executable = BUNDLED_SFE_PATH; + args = [VALIDATE_BLS_CHANGE_SUBCOMMAND]; + + args = args.concat([folder, chain.toLowerCase(), mnemonic, index.toString(), indices, + withdrawal_credentials, execution_address]); +} else if (await doesFileExist(SFE_PATH)) { + executable = SFE_PATH; + args = [VALIDATE_BLS_CHANGE_SUBCOMMAND]; + + args = args.concat([folder, chain.toLowerCase(), mnemonic, index.toString(), indices, + withdrawal_credentials, execution_address]); +} else { + if(!await requireDepositPackages()) { + throw new Error("Failed to generate BTEC, don't have the required packages."); + } + env.PYTHONPATH = await getPythonPath(); + + executable = PYTHON_EXE; + args = [STAKINGDEPOSIT_PROXY_PATH, VALIDATE_BLS_CHANGE_SUBCOMMAND]; + + args = args.concat([folder, chain.toLowerCase(), mnemonic, index.toString(), indices, + withdrawal_credentials, execution_address]); +} + +await execFileProm(executable, args, {env: env}); +} + +export { + createMnemonic, + generateKeys, + validateMnemonic, + validateBLSCredentials, + generateBLSChange +}; diff --git a/src/electron/preload.ts b/src/electron/preload.ts index 5c7bc351..ccc5fbb1 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.ts @@ -14,7 +14,7 @@ import { import Web3Utils from "web3-utils"; -import { createMnemonic, generateKeys, validateMnemonic } from "./Eth2Deposit"; +import { createMnemonic, generateKeys, validateMnemonic, validateBLSCredentials, generateBLSChange } from './Eth2Deposit'; import { doesDirectoryExist, @@ -42,10 +42,12 @@ contextBridge.exposeInMainWorld("electronAPI", { invokeShowOpenDialog: invokeShowOpenDialog, }); -contextBridge.exposeInMainWorld("eth2Deposit", { - createMnemonic: createMnemonic, - generateKeys: generateKeys, - validateMnemonic: validateMnemonic, +contextBridge.exposeInMainWorld('eth2Deposit', { + 'createMnemonic': createMnemonic, + 'generateKeys': generateKeys, + 'validateMnemonic': validateMnemonic, + 'validateBLSCredentials': validateBLSCredentials, + 'generateBLSChange': generateBLSChange }); contextBridge.exposeInMainWorld("bashUtils", { diff --git a/src/electron/renderer.d.ts b/src/electron/renderer.d.ts index dbda3441..115a977f 100644 --- a/src/electron/renderer.d.ts +++ b/src/electron/renderer.d.ts @@ -35,7 +35,9 @@ export interface IEth2DepositAPI { createMnemonic: (language: string) => Promise, generateKeys: (mnemonic: string, index: number, count: number, network: string, password: string, eth1_withdrawal_address: string, folder: string) => Promise, - validateMnemonic: (mnemonic: string) => Promise + validateMnemonic: (mnemonic: string) => Promise, + validateBLSCredentials: (chain: string, mnemonic: string, index: number, withdrawal_credentials: string) => Promise, + generateBLSChange: (folder: string, chain: string, mnemonic: string, index: number, indices: string, withdrawal_credentials: string, execution_address: string) => Promise, } export interface IBashUtilsAPI { diff --git a/src/react/App.tsx b/src/react/App.tsx index c349cc20..4e3d0540 100644 --- a/src/react/App.tsx +++ b/src/react/App.tsx @@ -6,7 +6,7 @@ import { CssBaseline, ThemeProvider } from "@material-ui/core"; import 'typeface-roboto'; import MainWizard from "./pages/MainWizard"; import theme from "./theme"; -import { Network } from './types'; +import { Network, ReuseMnemonicAction } from './types'; const Container = styled.main` display: flex; diff --git a/src/react/components/BTECConfigurationWizard.tsx b/src/react/components/BTECConfigurationWizard.tsx new file mode 100644 index 00000000..a8e42395 --- /dev/null +++ b/src/react/components/BTECConfigurationWizard.tsx @@ -0,0 +1,273 @@ +import { Grid, Typography } from '@material-ui/core'; +import React, { FC, ReactElement, useState, Dispatch, SetStateAction } from 'react'; +import styled from 'styled-components'; +import { errors } from '../constants'; +import MainInputs from './BTECGenerationFlow/0-MainInputs'; +import ValidatingBLSCredentials from './BTECGenerationFlow/1-ValidatingBLSCredentials'; +import StepNavigation from './StepNavigation'; + +const ContentGrid = styled(Grid)` + height: 320px; + margin-top: 16px; +`; + +type Props = { + onStepBack: () => void, + onStepForward: () => void, + network: string, + mnemonic: string, + startIndex: number, + setStartIndex: Dispatch>, + btecIndices: string, + setBtecIndices: Dispatch>, + btecCredentials: string, + setBtecCredentials: Dispatch>, + withdrawalAddress: string, + setWithdrawalAddress: Dispatch>, +} + +/** + * This is the wizard the user will navigate to configure their BLS to execution change. + * It uses the notion of a 'step' to render specific pages within the flow. + * + * @param props.onStepBack function to execute when stepping back + * @param props.onStepForward function to execute when stepping forward + * @param props.network the network for which to generate this BTEC + * @param props.mnemonic the current mnemonic for which to generate this BTEC + * @param props.startIndex the index for the keys to start generating withdrawal credentials + * @param props.setStartIndex function to set the starting index + * @param props.btecIndices a list of the chosen validator index number(s) as identified on the beacon chain + * @param props.setBtecIndices function to set the list of validator index number(s) + * @param props.btecCredentials a list of the old BLS withdrawal credentials of the given validator(s) + * @param props.setBtecCredentials function to set the list of old BLS withdrawal credentials + * @param props.withdrawalAddress the wallet address for the withdrawal credentials + * @param props.setWithdrawalAddress function to set the wallet address for the withdrawal credentials + * @returns the react element to render + */ +const BTECConfigurationWizard: FC = (props): ReactElement => { + const [step, setStep] = useState(0); + const [withdrawalAddressError, setWithdrawalAddressError] = useState(false); + const [withdrawalAddressErrorMsg, setWithdrawalAddressErrorMsg] = useState(""); + const [startingIndexError, setStartingIndexError] = useState(false); + const [indicesError, setIndicesError] = useState(false); + const [btecCredentialsError, setBtecCredentialsError] = useState(false); + const [indicesErrorMsg, setIndicesErrorMsg] = useState(""); + const [btecCredentialsErrorMsg, setBtecCredentialsErrorMsg] = useState(""); + + const prevLabel = () => { + switch (step) { + case 0: + return "Back"; + case 1: + return "Back"; + } + } + + const prevClicked = () => { + switch (step) { + case 0: { + setWithdrawalAddressError(false); + setWithdrawalAddressErrorMsg(""); + setStartingIndexError(false); + setIndicesError(false); + setIndicesErrorMsg(""); + setBtecCredentialsError(false); + setBtecCredentialsErrorMsg(""); + props.setStartIndex(0); + props.setBtecIndices(""); + props.setBtecCredentials(""); + props.setWithdrawalAddress(""); + props.onStepBack(); + break; + } + default: { + console.log("BTEC configuration step is greater than 0 when prev was clicked. This should never happen."); + break; + } + } + } + + const nextLabel = () => { + switch (step) { + case 0: + return "Next"; + case 1: + return "Next"; + } + } + + const nextClicked = () => { + switch (step) { + + // Inputs + case 0: { + validateInputs(); + break; + } + + default: { + console.log("BTEC configuration step is greater than 1 when next was clicked. This should never happen."); + break; + } + } + } + + const validateInputs = () => { + let isError = false; + + if (props.startIndex < 0) { + setStartingIndexError(true); + isError = true; + } else { + setStartingIndexError(false); + } + + const splitIndices = props.btecIndices.split(','); + const splitBLSCredentials = props.btecCredentials.split(','); + + if (props.btecIndices == "") { + setIndicesError(true); + setIndicesErrorMsg(errors.INDICES); + isError = true; + } else { + // Validate if all integers + + let indiceFormatError = false; + splitIndices.forEach( (indice) => { + if (!/^\d+$/.test(indice)) { + indiceFormatError = true; + } + }); + + if (indiceFormatError) { + setIndicesError(true); + setIndicesErrorMsg(errors.INDICES_FORMAT); + isError = true; + } else { + setIndicesError(false); + } + } + + if (props.btecCredentials == "") { + setBtecCredentialsError(true); + setBtecCredentialsErrorMsg(errors.BLS_CREDENTIALS); + isError = true; + } else { + // Validate if all credentials match format + + let credentialFormatError = false; + splitBLSCredentials.forEach( (credential) => { + if (!/^(0x)?00[0-9a-fA-F]{62}$/.test(credential)) { + credentialFormatError = true; + } + }); + + if (credentialFormatError) { + setBtecCredentialsError(true); + setBtecCredentialsErrorMsg(errors.BLS_CREDENTIALS_FORMAT); + isError = true; + } else { + setBtecCredentialsError(false); + } + } + + // Validate indices length matches credentials length + if (splitIndices.length != splitBLSCredentials.length) { + setIndicesError(true); + setIndicesErrorMsg(errors.INDICES_LENGTH); + isError = true; + } + + if (props.withdrawalAddress != "") { + if (!window.web3Utils.isAddress(props.withdrawalAddress)) { + setWithdrawalAddressError(true); + setWithdrawalAddressErrorMsg(errors.ADDRESS_FORMAT_ERROR); + isError = true; + } else { + setWithdrawalAddressError(false); + } + } else { + setWithdrawalAddressError(true); + setWithdrawalAddressErrorMsg(errors.WITHDRAW_ADDRESS_REQUIRED); + isError = true; + } + + if (!isError) { + + setStep(step + 1); + + window.eth2Deposit.validateBLSCredentials(props.network, props.mnemonic, props.startIndex, props.btecCredentials).then(() => { + props.onStepForward(); + }).catch((error) => { + setStep(0); + + setBtecCredentialsError(true); + setBtecCredentialsErrorMsg(errors.BLS_CREDENTIALS_NO_MATCH); + }) + } + } + + const content = () => { + switch(step) { + case 0: return ( + + ); + case 1: return ( + + ); + default: + return null; + } + } + + return ( + + + + Generate BLS to execution change + + + + + {content()} + + + {props.children} + + + ); +} + +export default BTECConfigurationWizard; \ No newline at end of file diff --git a/src/react/components/BTECGenerationFlow/0-MainInputs.tsx b/src/react/components/BTECGenerationFlow/0-MainInputs.tsx new file mode 100644 index 00000000..2bb67949 --- /dev/null +++ b/src/react/components/BTECGenerationFlow/0-MainInputs.tsx @@ -0,0 +1,148 @@ +import { Grid, TextField, Tooltip, Typography } from '@material-ui/core'; +import React, { Dispatch, SetStateAction } from 'react'; +import styled from "styled-components"; +import { errors, tooltips } from '../../constants'; + +type GenerateKeysProps = { + index: number, + setIndex: Dispatch>, + btecIndices: string, + setBtecIndices: Dispatch>, + btecCredentials: string, + setBtecCredentials: Dispatch>, + withdrawalAddress: string, + setWithdrawalAddress: Dispatch>, + withdrawalAddressError: boolean, + setWithdrawalAddressError: Dispatch>, + withdrawalAddressErrorMsg: string, + setWithdrawalAddressErrorMsg: Dispatch>, + startingIndexError: boolean, + setStartingIndexError: Dispatch>, + indicesError: boolean, + setIndicesError: Dispatch>, + indicesErrorMsg: string, + setIndicesErrorMsg: Dispatch>, + btecCredentialsError: boolean, + setBtecCredentialsError: Dispatch>, + btecCredentialsErrorMsg: string, + setBtecCredentialsErrorMsg: Dispatch>, + onFinish: () => void +} + +const IndexTextField = styled(TextField)` + width: 200px; +`; + +const IndiceTextField = styled(TextField)` + width: 560px; +` + +const BLSCredentialsField = styled(TextField)` + width: 680px; +` + +const AddressTextField = styled(TextField)` + margin: 12px 0; + width: 440px; +` + +const WithdrawalNotice = styled(Typography)` + margin: 0 60px; +` + +/** + * This page gathers data about the keys to generate for the user + * + * @param props self documenting parameters passed in + * @returns + */ +const MainInputs = (props: GenerateKeysProps) => { + + const updateIndex = (e: React.ChangeEvent) => { + const num = parseInt(e.target.value); + props.setIndex(num); + } + + const updateIndices = (e: React.ChangeEvent) => { + props.setBtecIndices(e.target.value.trim()); + } + + const updateBTECCredentials = (e: React.ChangeEvent) => { + props.setBtecCredentials(e.target.value.trim()); + } + + const updateEth1WithdrawAddress = (e: React.ChangeEvent) => { + props.setWithdrawalAddress(e.target.value.trim()); + } + + return ( + + + + + + + + + + + + + + + + + + + + + + Please ensure that you have control over this address. + + + + ); +} + +export default MainInputs; diff --git a/src/react/components/BTECGenerationFlow/1-ValidatingBLSCredentials.tsx b/src/react/components/BTECGenerationFlow/1-ValidatingBLSCredentials.tsx new file mode 100644 index 00000000..72f4dc43 --- /dev/null +++ b/src/react/components/BTECGenerationFlow/1-ValidatingBLSCredentials.tsx @@ -0,0 +1,42 @@ +import { Grid, Typography } from '@material-ui/core'; +import React, { FC, ReactElement } from 'react'; +import styled, { keyframes } from 'styled-components'; + +type ValidatingBLCCredentialsProps = { +} + +const spin = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +`; + +const Loader = styled.div` + border: 4px solid #f3f3f3; /* Light grey */ + border-top: 4px solid #3498db; /* Blue */ + border-radius: 50%; + width: 50px; + height: 50px; + animation: ${spin} 2s linear infinite; +`; + +/** + * This is the page that renders while we are validating the BLS credentials match the entered ones. + * + * @returns the react element to render + */ +const ValidatingBLCCredentials: FC = (): ReactElement => { + return ( + + + + Validating BLS credentials... + + + + + + + ); +} + +export default ValidatingBLCCredentials; \ No newline at end of file diff --git a/src/react/components/BTECGenerationFlow/2-SelectFolder.tsx b/src/react/components/BTECGenerationFlow/2-SelectFolder.tsx new file mode 100644 index 00000000..609bbcd1 --- /dev/null +++ b/src/react/components/BTECGenerationFlow/2-SelectFolder.tsx @@ -0,0 +1,74 @@ +import { Button, Grid, Typography } from '@material-ui/core'; +import { OpenDialogOptions, OpenDialogReturnValue } from 'electron'; +import React, { FC, ReactElement, Dispatch, SetStateAction } from 'react'; + +type SelectFolderProps = { + setFolderPath: Dispatch>, + folderPath: string, + setFolderError: Dispatch>, + folderError: boolean, + setFolderErrorMsg: Dispatch>, + folderErrorMsg: string, + setModalDisplay: Dispatch>, + modalDisplay: boolean, +} + +/** + * The page which prompts the user to choose a folder to save keys in + * + * @param props self documenting parameters passed in + * @returns react element to render + */ +const SelectFolder: FC = (props): ReactElement => { + const chooseFolder = () => { + props.setFolderError(false); + + const options: OpenDialogOptions = { + properties: ['openDirectory'] + }; + + props.setModalDisplay(true); + window.electronAPI.invokeShowOpenDialog(options) + .then((value: OpenDialogReturnValue) => { + if (value !== undefined && value.filePaths.length > 0) { + props.setFolderPath(value.filePaths[0]); + } else { + props.setFolderError(true); + } + }) + .finally(() => { + props.setModalDisplay(false); + }); + } + + return ( + + + + Choose a folder where we should save your BLS to execution change file. + + + + + + { props.folderPath != "" && + + + You've selected: {props.folderPath} + + + } + { props.folderError && + + + {props.folderErrorMsg} + + + } + + ); +} + +export default SelectFolder; \ No newline at end of file diff --git a/src/react/components/BTECGenerationFlow/3-CreatingBTECFile.tsx b/src/react/components/BTECGenerationFlow/3-CreatingBTECFile.tsx new file mode 100644 index 00000000..466e431d --- /dev/null +++ b/src/react/components/BTECGenerationFlow/3-CreatingBTECFile.tsx @@ -0,0 +1,50 @@ +import { Grid, Typography } from '@material-ui/core'; +import React, { FC, ReactElement } from 'react'; +import styled, { keyframes } from 'styled-components'; + +type CreatingBTECFileProps = { +} + +const spin = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +`; + +const Loader = styled.div` + border: 4px solid #f3f3f3; /* Light grey */ + border-top: 4px solid #3498db; /* Blue */ + border-radius: 50%; + width: 50px; + height: 50px; + animation: ${spin} 2s linear infinite; +`; + +const WaitingMessage = styled(Typography)` + align-items: center; +`; + +/** + * The waiting screen while the BTEC file is being created. + * + * @returns react element to render + */ +const CreatingBTECFile: FC = (): ReactElement => { + return ( + + + + + + Creating your BLS to execution change file. + + + + + + + + + ); +} + +export default CreatingBTECFile; \ No newline at end of file diff --git a/src/react/components/BTECGenerationFlow/4-BTECFileCreated.tsx b/src/react/components/BTECGenerationFlow/4-BTECFileCreated.tsx new file mode 100644 index 00000000..00a9fc7e --- /dev/null +++ b/src/react/components/BTECGenerationFlow/4-BTECFileCreated.tsx @@ -0,0 +1,71 @@ +import { Box, Grid, Typography, Link } from '@material-ui/core'; +import React, { FC, ReactElement } from 'react'; +import styled from 'styled-components'; +import { Network } from '../../types'; + +type BTECFileCreatedProps = { + folderPath: string, + network: Network +} + +const LoudText = styled(Typography)` + color: cyan; + text-align: left; +`; + +const QuietText = styled(Typography)` + color: gray; + text-align: left; +`; + +/** + * The final page displaying BTEC file location and information it. + * + * @param props self documenting paramenters passed in + * @returns the react element to render + */ +const BTECFileCreated: FC = (props): ReactElement => { + + const openKeyLocation = () => { + window.bashUtils.findFirstFile(props.folderPath, "bls_to_execution_change") + .then((keystoreFile) => { + let fileToLocate = props.folderPath; + if (keystoreFile != "") { + fileToLocate = keystoreFile; + } + window.electronAPI.shellShowItemInFolder(fileToLocate); + }); + } + + return ( + + + + + + Your BLS to execution change file has been created here: {props.folderPath} + + + + + + + + + There is a single file for this: + + BLS to execution file (ex. bls_to_execution_change-xxxxxxx.json) + + This file contains your signature to add your withdrawal address on your validator(s). You can easily publish it on beaconcha.in website by using their Broadcast Signed Messages tool. + + + Note: Your clipboard will be cleared upon closing this application. + + + + + + ); +} + +export default BTECFileCreated; \ No newline at end of file diff --git a/src/react/components/BTECGenerationWizard.tsx b/src/react/components/BTECGenerationWizard.tsx new file mode 100644 index 00000000..85f244ac --- /dev/null +++ b/src/react/components/BTECGenerationWizard.tsx @@ -0,0 +1,205 @@ +import { Grid, Typography } from '@material-ui/core'; +import React, { FC, ReactElement, Dispatch, SetStateAction, useState } from 'react'; +import styled from 'styled-components'; +import SelectFolder from './BTECGenerationFlow/2-SelectFolder'; +import CreatingBTECFile from './BTECGenerationFlow/3-CreatingBTECFile'; +import BTECFileCreated from './BTECGenerationFlow/4-BTECFileCreated'; +import StepNavigation from './StepNavigation'; +import { Network } from '../types'; +import { errors } from '../constants'; + +const ContentGrid = styled(Grid)` + height: 320px; + margin-top: 16px; +`; + +type Props = { + onStepBack: () => void, + onStepForward: () => void, + network: Network, + mnemonic: string, + startIndex: number, + withdrawalAddress: string, + btecIndices: string, + btecCredentials: string, + folderPath: string, + setFolderPath: Dispatch>, +} + +/** + * This is the wizard the user will navigate to generate their BTEC. + * It uses the notion of a 'step' to render specific pages within the flow. + * + * @param props.onStepBack function to execute when stepping back + * @param props.onStepForward function to execute when stepping forward + * @param props.network network the app is running for + * @param props.mnemonic the mnemonic + * @param props.startIndex the index at which to start generating BTEC for the user + * @param props.withdrawalAddress the wallet address for the withdrawal credentials + * @param props.btecIndices a list of the chosen validator index number(s) as identified on the beacon chain + * @param props.btecCredentials a list of the old BLS withdrawal credentials of the given validator(s) + * @param props.folderPath the path at which to store the keys + * @param props.setFolderPath funciton to update the path + * @returns the react element to render + */ +const BTECGenerationWizard: FC = (props): ReactElement => { + const [step, setStep] = useState(0); + const [folderError, setFolderError] = useState(false); + const [folderErrorMsg, setFolderErrorMsg] = useState(""); + const [modalDisplay, setModalDisplay] = useState(false); + + const prevLabel = () => { + switch (step) { + case 0: + return "Back"; + case 1: + return ""; // no back button + } + } + + const prevClicked = () => { + switch (step) { + case 0: { + props.setFolderPath(""); + setFolderError(false); + setFolderErrorMsg(""); + props.onStepBack(); + break; + } + default: { + console.log("BTEC generation step is greater than 0 and prev was clicked. This should never happen.") + break; + } + } + } + + const nextLabel = () => { + switch (step) { + case 0: + return "Create"; + case 1: + return ""; // no next button + } + } + + const nextClicked = () => { + switch (step) { + // Select Folder + case 0: { + if (props.folderPath != "") { + setFolderError(false); + setFolderErrorMsg(""); + + window.bashUtils.doesDirectoryExist(props.folderPath) + .then((exists) => { + if (!exists) { + setFolderErrorMsg(errors.FOLDER_DOES_NOT_EXISTS); + setFolderError(true); + } else { + + window.bashUtils.isDirectoryWritable(props.folderPath) + .then((writable) => { + if (!writable) { + setFolderErrorMsg(errors.FOLDER_IS_NOT_WRITABLE); + setFolderError(true); + } else { + setStep(step + 1); + handleBTECFileGeneration(); + } + }); + } + }); + + } else { + setFolderError(true); + setFolderErrorMsg(errors.FOLDER); + } + + break; + } + + // BTEC file is being generated + case 1: { + // there is no next button here + // step is autoincremented once file is created + break; + } + + default: { + console.log("BTEC generation step is greater than 1 and next was clicked. This should never happen.") + break; + } + + } + } + + const handleBTECFileGeneration = () => { + + let withdrawalAddress = props.withdrawalAddress; + + if (withdrawalAddress != "" && !withdrawalAddress.toLowerCase().startsWith("0x")) { + withdrawalAddress = "0x" + withdrawalAddress; + } + + window.eth2Deposit.generateBLSChange( + props.folderPath, + props.network, + props.mnemonic, + props.startIndex, + props.btecIndices, + props.btecCredentials, + withdrawalAddress).then(() => { + props.onStepForward(); + }).catch((error) => { + setStep(0); + setFolderError(true); + const errorMsg = ('stderr' in error) ? error.stderr : error.message; + setFolderErrorMsg(errorMsg); + }) + + } + + const content = () => { + switch(step) { + case 0: return ( + + ); + case 1: return ( + + ); + case 2: return ( + + ); + default: + return null; + } + } + + return ( + + + + Generate BLS to execution change + + + + + {content()} + + + {props.children} + + + ); +} + +export default BTECGenerationWizard; \ No newline at end of file diff --git a/src/react/components/Finish.tsx b/src/react/components/Finish.tsx index 5d03a818..f0553c60 100644 --- a/src/react/components/Finish.tsx +++ b/src/react/components/Finish.tsx @@ -40,7 +40,7 @@ const Finish: FC = (props): ReactElement => { - Create Keys + Done! @@ -65,4 +65,4 @@ const Finish: FC = (props): ReactElement => { ); } -export default Finish; \ No newline at end of file +export default Finish; diff --git a/src/react/components/FinishBTEC.tsx b/src/react/components/FinishBTEC.tsx new file mode 100644 index 00000000..f1dfc351 --- /dev/null +++ b/src/react/components/FinishBTEC.tsx @@ -0,0 +1,53 @@ +import { Button, Grid, Typography } from '@material-ui/core'; +import React, { FC, ReactElement, Dispatch, SetStateAction } from 'react'; +import styled from 'styled-components'; +import { Network } from '../types'; +import BTECFileCreated from './BTECGenerationFlow/4-BTECFileCreated'; + +const ContentGrid = styled(Grid)` + height: 320px; + margin-top: 16px; +`; + +type Props = { + onStepBack: () => void, + onStepForward: () => void, + folderPath: string, + network: Network +} + +/** + * This is the final page displaying information about the keys + * + * @param props.onStepBack the function to execute when the user steps back + * @param props.onStepForward the function to execute when the user steps forward + * @param props.folderPath the folder path where the keys are located, for display purposes + * @param props.network the network the app is running for + * @returns the react element to render + */ +const FinishBTEC: FC = (props): ReactElement => { + return ( + + + + Generate BLS to execution change + + + + + + + + {props.children} + + + + + + + + + ); +} + +export default FinishBTEC; \ No newline at end of file diff --git a/src/react/components/KeyConfigurationWizard.tsx b/src/react/components/KeyConfigurationWizard.tsx index 4d82e880..e76206b4 100644 --- a/src/react/components/KeyConfigurationWizard.tsx +++ b/src/react/components/KeyConfigurationWizard.tsx @@ -23,8 +23,6 @@ type Props = { setWithdrawalAddress: Dispatch>, password: string, setPassword: Dispatch>, - showAdvanced: boolean, - setShowAdvanced: Dispatch> } /** @@ -71,7 +69,6 @@ const KeyConfigurationWizard: FC = (props): ReactElement => { setWithdrawalAddressFormatError(false); setPasswordStrengthError(false); setStartingIndexError(false); - props.setShowAdvanced(false); props.setPassword(""); props.setKeyGenerationStartIndex(props.initialKeyGenerationStartIndex); props.setNumberOfKeys(1); @@ -149,7 +146,7 @@ const KeyConfigurationWizard: FC = (props): ReactElement => { setStartingIndexError(false); } - if (props.showAdvanced) { //props.withdrawalAddress != "" && + if (props.withdrawalAddress != "") { if (!window.web3Utils.isAddress(props.withdrawalAddress)) { setWithdrawalAddressFormatError(true); isError = true; @@ -192,8 +189,6 @@ const KeyConfigurationWizard: FC = (props): ReactElement => { setWithdrawalAddressFormatError={setWithdrawalAddressFormatError} passwordStrengthError={passwordStrengthError} startingIndexError={startingIndexError} - showAdvanced={props.showAdvanced} - setShowAdvanced={props.setShowAdvanced} onFinish={validateInputs} /> ); diff --git a/src/react/components/KeyGeneratioinFlow/0-KeyInputs.tsx b/src/react/components/KeyGeneratioinFlow/0-KeyInputs.tsx index a67aadcc..9c2e83bf 100644 --- a/src/react/components/KeyGeneratioinFlow/0-KeyInputs.tsx +++ b/src/react/components/KeyGeneratioinFlow/0-KeyInputs.tsx @@ -1,5 +1,5 @@ -import { Button, Fade, FormControlLabel, Grid, Switch, TextField, Tooltip, Typography } from '@material-ui/core'; -import React, { Dispatch, SetStateAction, useState } from 'react'; +import { Fade, FormControlLabel, Grid, Switch, TextField, Tooltip, Typography } from '@material-ui/core'; +import React, { Dispatch, SetStateAction } from 'react'; import styled from "styled-components"; import { errors, tooltips } from '../../constants'; @@ -18,8 +18,6 @@ type GenerateKeysProps = { numberOfKeysError: boolean, passwordStrengthError: boolean, startingIndexError: boolean, - showAdvanced: boolean, - setShowAdvanced: Dispatch>, onFinish: () => void } @@ -34,6 +32,10 @@ const AddressTextField = styled(TextField)` width: 440px; ` +const WithdrawalNotice = styled(Typography)` + margin: 0 60px; +` + /** * This page gathers data about the keys to generate for the user * @@ -41,14 +43,6 @@ const AddressTextField = styled(TextField)` * @returns */ const KeyInputs = (props: GenerateKeysProps) => { - - const handleToggleShowAdvanced = () => { - props.setShowAdvanced(!props.showAdvanced); - if (!props.showAdvanced) { - props.setWithdrawalAddress(""); - props.setWithdrawalAddressFormatError(false); - } - } const updateNumberOfKeys = (e: React.ChangeEvent) => { const num = parseInt(e.target.value); @@ -124,32 +118,24 @@ const KeyInputs = (props: GenerateKeysProps) => { - } - label="Use Advanced Inputs" - /> - - - - - - - - - - Please ensure that you have control over this address. - - + + + + + + + Please ensure that you have control over this address. If you do not add a withdrawal address now, you will be able to add one later with your 24 words secret recovery phrase. + - + ); diff --git a/src/react/components/KeyGenerationFlow/0-KeyInputs.tsx b/src/react/components/KeyGenerationFlow/0-KeyInputs.tsx index 273f0168..5e128c89 100644 --- a/src/react/components/KeyGenerationFlow/0-KeyInputs.tsx +++ b/src/react/components/KeyGenerationFlow/0-KeyInputs.tsx @@ -18,8 +18,6 @@ type GenerateKeysProps = { numberOfKeysError: boolean, passwordStrengthError: boolean, startingIndexError: boolean, - showAdvanced: boolean, - setShowAdvanced: Dispatch>, onFinish: () => void } diff --git a/src/react/components/MnemonicGenerationFlow/1-2-ShowMnemonic.tsx b/src/react/components/MnemonicGenerationFlow/1-2-ShowMnemonic.tsx index 2283488e..1b265358 100644 --- a/src/react/components/MnemonicGenerationFlow/1-2-ShowMnemonic.tsx +++ b/src/react/components/MnemonicGenerationFlow/1-2-ShowMnemonic.tsx @@ -1,7 +1,6 @@ -import { Tooltip, IconButton, Grid, Typography, CircularProgress, TextField, withStyles } from '@material-ui/core'; -import Alert from '@material-ui/lab/Alert'; +import { Tooltip, IconButton, Grid, Typography, TextField, withStyles } from '@material-ui/core'; import { FileCopy } from '@material-ui/icons'; -import React, { FC, ReactElement, Fragment, useState } from 'react'; +import React, { FC, ReactElement, useState } from 'react'; import styled, { keyframes } from 'styled-components'; import { Primary, Warning } from '../../colors' import { Network } from '../../types'; @@ -93,9 +92,9 @@ const ShowMnemonic: FC = (props): ReactElement => { return ( { props.mnemonic == "" && - + - Generating your secret recovery phrase. May take up to 30 seconds. + Generating your secret recovery phrase. May take up to 30 seconds. diff --git a/src/react/components/MnemonicImport.tsx b/src/react/components/MnemonicImport.tsx index e1a8d585..a170064d 100644 --- a/src/react/components/MnemonicImport.tsx +++ b/src/react/components/MnemonicImport.tsx @@ -29,7 +29,7 @@ const MnemonicImport: FC = (props): ReactElement => { const [mnemonicErrorMsg, setMnemonicErrorMsg] = useState(""); const updateInputMnemonic = (e: React.ChangeEvent) => { - props.setMnemonic(e.target.value); + props.setMnemonic(e.target.value.trim()); } const disableImport = !props.mnemonic; @@ -55,7 +55,7 @@ const MnemonicImport: FC = (props): ReactElement => { setMnemonicError(true); setMnemonicErrorMsg(errorMsg); }) - + } } @@ -118,4 +118,4 @@ const MnemonicImport: FC = (props): ReactElement => { ) } -export default MnemonicImport; \ No newline at end of file +export default MnemonicImport; diff --git a/src/react/components/ReuseMnemonicActionPicker.tsx b/src/react/components/ReuseMnemonicActionPicker.tsx new file mode 100644 index 00000000..6fe901a2 --- /dev/null +++ b/src/react/components/ReuseMnemonicActionPicker.tsx @@ -0,0 +1,87 @@ +import { BackgroundLight, } from '../colors'; +import { Button, Grid, Tooltip } from '@material-ui/core'; +import React from 'react'; + +import { ReuseMnemonicAction } from '../types'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + flex-direction: column; + height: 320px; + width: 560px; + background: rgba(27, 38, 44, 0.95); + border-radius: 20px; + align-items: center; + background: ${BackgroundLight}; + margin: auto; + margin-top: 150px; +`; + +const Header = styled.div` + font-size: 26px; + margin-top: 30px; + margin-bottom: 30px; + margin-left: 20px; + margin-right: 20px; +`; + +const OptionsGrid = styled(Grid)` + margin-top: 15px; + align-items: center; +`; + +type ReuseMnemonicActionPickerProps = { + handleCloseReuseMnemonicModal: (event: object, reason: string) => void, + handleReuseMnemonicModalSubmitClick: (action: ReuseMnemonicAction) => void, +} + +/** + * This is the reuse mnemonic action picker modal component where the user selects which action they want + * to perform after reusing their mnemonic. + * + * @param props.handleCloseReuseMnemonicModal function to handle closing the reuse mnemonic action modal + * @param props.setReuseMnemonicAction update the selected reuse mnemonic action + * @param props.reuseMnemonicAction the selected reuse mnemonic action + * @returns the reuse mnemonic action picker element to render + */ +export const ReuseMnemonicActionPicker = (props: ReuseMnemonicActionPickerProps) => { + + const handleSelectRegenerateKeys = () => { + + const action = ReuseMnemonicAction.RegenerateKeys; + props.handleReuseMnemonicModalSubmitClick(action); + props.handleCloseReuseMnemonicModal({}, 'submitClick'); + + } + + const handleSelectGenerateBLSToExecutionChange = () => { + + const action = ReuseMnemonicAction.GenerateBLSToExecutionChange; + props.handleReuseMnemonicModalSubmitClick(action); + props.handleCloseReuseMnemonicModal({}, 'submitClick'); + + } + + return ( + +
How would you like to use your existing secret recovery phrase?
+
+ + + + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/src/react/constants.ts b/src/react/constants.ts index b82055f6..e9c314eb 100644 --- a/src/react/constants.ts +++ b/src/react/constants.ts @@ -1,39 +1,47 @@ import { StepKey } from "./types"; export const errors = { - MNEMONIC_FORMAT: - "Invalid format. Your Secret Recovery Phrase should be a 24 word list.", - MNEMONICS_DONT_MATCH: - "The Secret Recovery Phrase you entered does not match what was given to you. Please try again.", - NUMBER_OF_KEYS: "Please input a number between 1 and 1000.", - ADDRESS_FORMAT_ERROR: "Please enter a valid LUKSO address.", - PASSWORD_STRENGTH: "Password must be at least 8 characters.", - PASSWORD_MATCH: "Passwords don't match.", - STARTING_INDEX: "Please input starting index.", - FOLDER: "Please select a folder.", - FOLDER_DOES_NOT_EXISTS: "Folder does not exist. Select an existing folder.", - FOLDER_IS_NOT_WRITABLE: - "Cannot write in this folder. Select a folder in which you have write permission.", + MNEMONIC_FORMAT: "Invalid format. Your Secret Recovery Phrase should be a 24 word list.", + MNEMONICS_DONT_MATCH: "The Secret Recovery Phrase you entered does not match what was given to you. Please try again.", + NUMBER_OF_KEYS: "Please input a number between 1 and 1000.", + ADDRESS_FORMAT_ERROR: "Please enter a valid Ethereum address.", + WITHDRAW_ADDRESS_REQUIRED: "Please enter an Ethereum address.", + PASSWORD_STRENGTH: "Password must be at least 8 characters.", + PASSWORD_MATCH: "Passwords don't match.", + STARTING_INDEX: "Please input start index.", + INDICES: "Please input indices.", + INDICES_FORMAT: "Please input indices with digits only.", + INDICES_LENGTH: "The amount of indices must match the amount of BLS credentials", + BLS_CREDENTIALS: "Please input BLS credentials.", + BLS_CREDENTIALS_FORMAT: "Please enter valid BLS credentials.", + BLS_CREDENTIALS_NO_MATCH: "Those BLS credentials do not match those we can derive from your Secret Recovery Phrase.", + FOLDER: "Please select a folder.", + FOLDER_DOES_NOT_EXISTS: "Folder does not exist. Select an existing folder.", + FOLDER_IS_NOT_WRITABLE: "Cannot write in this folder. Select a folder in which you have write permission.", }; export const MNEMONIC_LENGTH = 24; export const tooltips = { - IMPORT_MNEMONIC: - "If you've already created a Secret Recovery Phrase, you can create more keys from it by importing it here.", - NUMBER_OF_KEYS: "Enter how many new validator keys you'd like to create.", - PASSWORD: - "Pick a strong password (at least 8 characters) that will be used to protect your keys.", - STARTING_INDEX: - "Each key is created sequentially, so we need to know how many you've created with this Secret Recovery Phrase in the past in order to create some new ones for you.", - ETH1_WITHDRAW_ADDRESS: - "An LUKSO address for the validator payout and withdraw of validator balances.", + IMPORT_MNEMONIC: "If you've already created a Secret Recovery Phrase, you can use it to regenerate your original keys, create more keys, or generate a BLS to execution change by importing the phrase here.", + NUMBER_OF_KEYS: "Enter how many new validator keys you'd like to create.", + PASSWORD: "Pick a strong password (at least 8 characters) that will be used to protect your keys.", + STARTING_INDEX: "Each key is created sequentially, so we need to know how many you've created with this Secret Recovery Phrase in the past in order to create some new ones for you.", + ETH1_WITHDRAW_ADDRESS: "An optional Ethereum address for the withdrawal credentials.", + BTEC_WITHDRAW_ADDRESS: "An Ethereum address for withdrawal. There is where your validator balance and rewards will go.", + OFFLINE: "You want to avoid exposing your Secret Recovery Phrase to any system that can send it online and compromise your security. Booting from a live OS that does not connect to any network on a USB drive is an easy way to achieve that. You can copy the resulting files on USB drives. You might want to avoid storing your Secret Recovery Phrase electronically.", + BTEC_START_INDEX: "The index position for the keys to start generating withdrawal credentials. If you only created 1 validator key using this Secret Recovery Phrase, this is likely going to be 0. If you created many validator keys, this could be a higher value from where you want to start in the list of validators derived from your Secret Recovery Phrase.", + BTEC_INDICES: "A list of the chosen validator index number(s) as identified on the beacon chain. You can find your validator indice on beaconcha.in website on your validator page. It will be at the top of that page the form of a title like Validator XXXXX, where XXXXX is going to be your indice.", + BLS_CREDENTIALS: "A list of the old BLS withdrawal credentials of the given validator(s). You can find your validator BLS withdrawal credentials on beaconcha.in website on your validator page. It will be in the Deposits tab and it should start with 0x00.", }; export const stepLabels = { - [StepKey.MnemonicImport]: "Import Secret Recovery Phrase", - [StepKey.MnemonicGeneration]: "Create Secret Recovery Phrase", - [StepKey.KeyConfiguration]: "Configure Validator Keys", - [StepKey.KeyGeneration]: "Create Validator Key Files", - [StepKey.Finish]: "Finish", + [StepKey.MnemonicImport]: 'Import Secret Recovery Phrase', + [StepKey.MnemonicGeneration]: 'Create Secret Recovery Phrase', + [StepKey.KeyConfiguration]: 'Configure Validator Keys', + [StepKey.KeyGeneration]: 'Create Validator Key Files', + [StepKey.Finish]: 'Finish', + [StepKey.BTECConfiguration]: 'Configure Withdrawal Address', + [StepKey.BTECGeneration]: 'Create Crendentials Change', + [StepKey.FinishBTEC]: 'Finish' }; diff --git a/src/react/pages/Home.tsx b/src/react/pages/Home.tsx index 10939267..78b5ecea 100644 --- a/src/react/pages/Home.tsx +++ b/src/react/pages/Home.tsx @@ -5,8 +5,9 @@ import { Container, Grid, Modal, Tooltip, Typography } from "@material-ui/core"; import { Button } from '@material-ui/core'; import { KeyIcon } from "../components/icons/KeyIcon"; import { NetworkPicker } from "../components/NetworkPicker"; +import { ReuseMnemonicActionPicker } from "../components/ReuseMnemonicActionPicker"; import { tooltips } from "../constants"; -import { Network, StepSequenceKey } from '../types' +import { Network, StepSequenceKey, ReuseMnemonicAction } from '../types' import VersionFooter from "../components/VersionFooter"; import logo from "../../../static/keyVisual.png"; @@ -47,7 +48,7 @@ const BackgroundImage = styled.img` `; const Links = styled.div` - margin-top: 35px; + margin-top: 20px; `; const LinksTag = styled.a` color: #a3aada; @@ -58,10 +59,14 @@ const InfoLabel = styled.span` `; const OptionsGrid = styled(Grid)` - margin-top: 35px; + margin-top: 20px; align-items: center; `; +const Dotted = styled.span` + text-decoration-line: underline; +`; + type HomeProps = { network: Network, setNetwork: Dispatch> @@ -78,6 +83,7 @@ type HomeProps = { const Home: FC = (props): ReactElement => { const [showNetworkModal, setShowNetworkModal] = useState(false); const [networkModalWasOpened, setNetworkModalWasOpened] = useState(false); + const [showReuseMnemonicModal, setShowReuseMnemonicModal] = useState(false); const [createMnemonicSelected, setCreateMnemonicSelected] = useState(false); const [useExistingMnemonicSelected, setUseExistingMnemonicSelected] = useState(false); @@ -89,7 +95,7 @@ const Home: FC = (props): ReactElement => { } const handleCloseNetworkModal = (event: object, reason: string) => { - if (reason !== 'backdropClick') { + if (reason == 'submitClick') { setShowNetworkModal(false); if (createMnemonicSelected) { @@ -100,6 +106,36 @@ const Home: FC = (props): ReactElement => { } } + const handleOpenReuseMnemonicModal = () => { + setShowReuseMnemonicModal(true); + } + + const handleReuseMnemonicModalSubmitClick = (action: ReuseMnemonicAction) => { + + if (action == ReuseMnemonicAction.RegenerateKeys) { + + const location = { + pathname: `/wizard/${StepSequenceKey.MnemonicImport}` + } + + history.push(location); + + } else if (action == ReuseMnemonicAction.GenerateBLSToExecutionChange) { + + const location = { + pathname: `/wizard/${StepSequenceKey.BLSToExecutionChangeGeneration}` + } + + history.push(location); + + } + + } + + const handleCloseReuseMnemonicModal = (event: object, reason: string) => { + setShowReuseMnemonicModal(false); + } + const handleCreateNewMnemonic = () => { setCreateMnemonicSelected(true); @@ -120,11 +156,9 @@ const Home: FC = (props): ReactElement => { if (!networkModalWasOpened) { handleOpenNetworkModal(); } else { - const location = { - pathname: `/wizard/${StepSequenceKey.MnemonicImport}` - } - history.push(location); + handleOpenReuseMnemonicModal(); + } } @@ -146,12 +180,21 @@ const Home: FC = (props): ReactElement => { + + {/* Added
here per the following link to fix error https://stackoverflow.com/a/63521049/5949270 */} +
+ +
+ LUKSO
Wagyu KeyGen
{/* */} Your key generator for staking on LUKSO - + offline for your own security. diff --git a/src/react/pages/MainWizard.tsx b/src/react/pages/MainWizard.tsx index 752ff2f9..32e7b54b 100644 --- a/src/react/pages/MainWizard.tsx +++ b/src/react/pages/MainWizard.tsx @@ -8,6 +8,9 @@ import MnemonicImport from "../components/MnemonicImport"; import KeyConfigurationWizard from "../components/KeyConfigurationWizard"; import KeyGenerationWizard from "../components/KeyGenerationWizard"; import Finish from '../components/Finish'; +import BTECConfigurationWizard from "../components/BTECConfigurationWizard"; +import BTECGenerationWizard from "../components/BTECGenerationWizard"; +import FinishBTEC from '../components/FinishBTEC'; import { stepLabels } from '../constants'; import { Network, StepSequenceKey } from '../types'; import VersionFooter from '../components/VersionFooter'; @@ -25,6 +28,12 @@ const stepSequenceMap: Record = { StepKey.KeyConfiguration, StepKey.KeyGeneration, StepKey.Finish + ], + blstoexecutionchangegeneration: [ + StepKey.MnemonicImport, + StepKey.BTECConfiguration, + StepKey.BTECGeneration, + StepKey.FinishBTEC ] } @@ -70,12 +79,14 @@ const Wizard: FC = (props): ReactElement => { const [mnemonic, setMnemonic] = useState(""); const [mnemonicToVerify, setMnemonicToVerify] = useState(""); const [activeStepIndex, setActiveStepIndex] = useState(0); - const [keyGenerationStartIndex, setKeyGenerationStartIndex] = useState(0); + const [startIndex, setStartIndex] = useState(0); const [numberOfKeys, setNumberOfKeys] = useState(1); const [withdrawalAddress, setWithdrawalAddress] = useState(""); const [password, setPassword] = useState(""); const [folderPath, setFolderPath] = useState(""); const [showAdvanced, setShowAdvanced] = useState(true); + const [btecIndices, setBtecIndices] = useState(""); + const [btecCredentials, setBtecCredentials] = useState(""); const stepSequence = stepSequenceMap[stepSequenceKey]; const activeStepKey = stepSequence[activeStepIndex]; @@ -137,9 +148,9 @@ const Wizard: FC = (props): ReactElement => { return ( = (props): ReactElement => { setWithdrawalAddress={setWithdrawalAddress} password={password} setPassword={setPassword} - showAdvanced={showAdvanced} - setShowAdvanced={setShowAdvanced} /> ); - case StepKey.KeyGeneration: - return ( - - ); - case StepKey.Finish: - return ( - - ) + case StepKey.KeyGeneration: + return ( + + ); + case StepKey.Finish: + return ( + + ); + case StepKey.BTECConfiguration: + return ( + + ); + case StepKey.BTECGeneration: + return ( + + ); + case StepKey.FinishBTEC: + return ( + + ); default: return
No component for this step
} diff --git a/src/react/types.ts b/src/react/types.ts index ea9b7ef2..b0080647 100644 --- a/src/react/types.ts +++ b/src/react/types.ts @@ -4,11 +4,20 @@ export enum StepKey { KeyConfiguration, KeyGeneration, Finish, + BTECConfiguration, + BTECGeneration, + FinishBTEC } export enum StepSequenceKey { MnemonicGeneration = "mnemonicgeneration", MnemonicImport = "mnemonicimport", + BLSToExecutionChangeGeneration = "blstoexecutionchangegeneration", +} + +export enum ReuseMnemonicAction { + RegenerateKeys, + GenerateBLSToExecutionChange } // Networks will be lowercased and passed in as parameters to the deposit-cli diff --git a/src/scripts/bundle_proxy_linux.sh b/src/scripts/bundle_proxy_linux.sh index c3c5af03..db2fffa8 100755 --- a/src/scripts/bundle_proxy_linux.sh +++ b/src/scripts/bundle_proxy_linux.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Bash script to bundle the eth2deposit_proxy application and the associated required files on +# Bash script to bundle the stakingdeposit_proxy application and the associated required files on # Linux and macOS. if [ -f ~/.bash_aliases ]; then @@ -30,13 +30,13 @@ mkdir -p $TARGETPACKAGESPATH python -m pip install pip pyinstaller -U --user python3 -m pip install -r $ETH2REQUIREMENTSPATH --target $TARGETPACKAGESPATH --no-deps -# Bundling Python eth2deposit_proxy +# Bundling Python stakingdeposit_proxy PYTHONPATH=$PYTHONPATH pyinstaller \ --onefile \ --distpath $DISTBINPATH \ --add-data "$SRCINTLPATH:staking_deposit/intl" \ -p $PYTHONPATH \ - $SCRIPTPATH/eth2deposit_proxy.py + $SCRIPTPATH/stakingdeposit_proxy.py # Adding word list cp $SRCWORDSPATH/* $DISTWORDSPATH diff --git a/src/scripts/bundle_proxy_mac.sh b/src/scripts/bundle_proxy_mac.sh index 2a98bd63..14705f20 100755 --- a/src/scripts/bundle_proxy_mac.sh +++ b/src/scripts/bundle_proxy_mac.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Bash script to bundle the eth2deposit_proxy application and the associated required files on +# Bash script to bundle the stakingdeposit_proxy application and the associated required files on # Linux and macOS. if [ -f ~/.bash_aliases ]; then @@ -43,26 +43,29 @@ export ARCHFLAGS='-arch arm64 -arch x86_64' VERSION=$(sed -n -e 's#\(pycryptodome==[^ ]*\).*#\1#gp' $ETH2REQUIREMENTSPATH) echo $VERSION + python3 -m pip install pip -U python3 -m pip install cython --no-binary :all: --target $TARGETPACKAGESMACPATH python3 -m pip install $VERSION --no-binary :all: --target $TARGETPACKAGESMACPATH python3 -m pip install cytoolz==0.12.1 --no-binary :all: --target $TARGETPACKAGESMACPATH +python3 -m pip install -r ./src/vendors/tools-key-gen-cli/build_configs/macos/requirements.txt --target $TARGETPACKAGESMACPATH +python3 -m pip install -r ./src/vendors/tools-key-gen-cli/build_configs/macos/requirements.pyinstaller.txt --target $TARGETPACKAGESMACPATH python3 -m pip install -r $ETH2REQUIREMENTSPATH --target $TARGETPACKAGESPATH --no-deps -python3 -m pip install pyinstaller +python3 -m pip install pyinstaller --target $TARGETPACKAGESPATH -# Bundling Python eth2deposit_proxy +# Bundling Python stakingdeposit_proxy PYTHONPATH=$PYTHONPATH pyinstaller \ --distpath $DISTX64PATH \ --target-arch x86_64 \ --add-data "$SRCINTLPATH:staking_deposit/intl" \ -p $PYTHONPATH \ - $SCRIPTPATH/eth2deposit_proxy.py + $SCRIPTPATH/stakingdeposit_proxy.py PYTHONPATH=$PYTHONPATH pyinstaller \ --distpath $DISTARMPATH \ --target-arch arm64 \ --add-data "$SRCINTLPATH:staking_deposit/intl" \ -p $PYTHONPATH \ - $SCRIPTPATH/eth2deposit_proxy.py + $SCRIPTPATH/stakingdeposit_proxy.py # Adding word list cp $SRCWORDSPATH/* $DISTWORDSPATH diff --git a/src/scripts/bundle_proxy_win.bat b/src/scripts/bundle_proxy_win.bat index 8f67b1c7..aac7a021 100644 --- a/src/scripts/bundle_proxy_win.bat +++ b/src/scripts/bundle_proxy_win.bat @@ -1,6 +1,6 @@ @ECHO OFF -rem Batch script to bundle the eth2deposit_proxy application and the associated required files on +rem Batch script to bundle the stakingdeposit_proxy application and the associated required files on rem Windows. SET BATDIR=%~dp0 @@ -28,8 +28,8 @@ rem Getting all the requirements python -m pip install pip pyinstaller -U --user python -m pip install -r %ETH2REQUIREMENTSPATH% --target %TARGETPACKAGESPATH% --no-deps -U -rem Bundling Python eth2deposit_proxy -pyinstaller --onefile --distpath %DISTBINPATH% --add-data "%SRCINTLPATH%;staking_deposit\intl" -p %PYTHONPATH% %BATDIR%eth2deposit_proxy.py +rem Bundling Python stakingdeposit_proxy +pyinstaller --onefile --distpath %DISTBINPATH% --add-data "%SRCINTLPATH%;staking_deposit\intl" -p %PYTHONPATH% %BATDIR%stakingdeposit_proxy.py rem Adding word list copy /Y %SRCWORDSPATH%\* %DISTWORDSPATH% diff --git a/src/scripts/eth2deposit_proxy.py b/src/scripts/eth2deposit_proxy.py deleted file mode 100644 index 4efdd793..00000000 --- a/src/scripts/eth2deposit_proxy.py +++ /dev/null @@ -1,171 +0,0 @@ -"""The eth2deposit_proxy application. - -This application is used as a proxy between our electron application and the eth2-deposit-cli -internals. It exposes some eth2-deposit-cli functions as easy to use commands that can be called -on the CLI. -""" - -import os -import argparse -import json -import sys - -from staking_deposit.key_handling.key_derivation.mnemonic import ( - get_mnemonic, - reconstruct_mnemonic -) - -from eth_utils import is_hex_address, to_normalized_address - -from staking_deposit.credentials import ( - CredentialList, -) - -from staking_deposit.exceptions import ValidationError -from staking_deposit.utils.validation import ( - verify_deposit_data_json, -) -from staking_deposit.utils.constants import ( - MAX_DEPOSIT_AMOUNT, -) - -from staking_deposit.settings import ( - get_chain_setting, -) - -def validate_mnemonic(mnemonic: str, word_lists_path: str) -> str: - """Validate a mnemonic using the eth2-deposit-cli logic and returns the mnemonic. - - Keyword arguments: - mnemonic -- the mnemonic to validate - word_lists_path -- path to the word lists directory - """ - - mnemonic = reconstruct_mnemonic(mnemonic, word_lists_path) - if mnemonic is not None: - return mnemonic - else: - raise ValidationError('That is not a valid mnemonic, please check for typos.') - -def create_mnemonic(word_list, language='english'): - """Returns a new random mnemonic. - - Keyword arguments: - word_lists -- path to the word lists directory - language -- the language for the mnemonic words, possible values are 'chinese_simplified', - 'chinese_traditional', 'czech', 'english', 'italian', 'korean', 'portuguese' or - 'spanish' (default 'english') - """ - return get_mnemonic(language=language, words_path=word_list) - -def generate_keys(args): - """Generate validator keys. - - Keyword arguments: - args -- contains the CLI arguments pass to the application, it should have those properties: - - wordlist: path to the word lists directory - - mnemonic: mnemonic to be used as the seed for generating the keys - - index: index of the first validator's keys you wish to generate - - count: number of signing keys you want to generate - - folder: folder path for the resulting keystore(s) and deposit(s) files - - network: network setting for the signing domain, possible values are 'mainnet', - 'prater', 'kintsugi' or 'kiln' - - password: password that will protect the resulting keystore(s) - - eth1_withdrawal_address: (Optional) eth1 address that will be used to create the - withdrawal credentials - """ - - eth1_withdrawal_address = None - if args.eth1_withdrawal_address: - eth1_withdrawal_address = args.eth1_withdrawal_address - if not is_hex_address(eth1_withdrawal_address): - raise ValueError("The given Eth1 address is not in hexadecimal encoded form.") - - eth1_withdrawal_address = to_normalized_address(eth1_withdrawal_address) - - mnemonic = validate_mnemonic(args.mnemonic, args.wordlist) - mnemonic_password = '' - amounts = [MAX_DEPOSIT_AMOUNT] * args.count - folder = args.folder - chain_setting = get_chain_setting(args.network) - if not os.path.exists(folder): - os.mkdir(folder) - - credentials = CredentialList.from_mnemonic( - mnemonic=mnemonic, - mnemonic_password=mnemonic_password, - num_keys=args.count, - amounts=amounts, - chain_setting=chain_setting, - start_index=args.index, - hex_eth1_withdrawal_address=eth1_withdrawal_address, - ) - - keystore_filefolders = credentials.export_keystores(password=args.password, folder=folder) - deposits_file = credentials.export_deposit_data_json(folder=folder) - if not credentials.verify_keystores(keystore_filefolders=keystore_filefolders, password=args.password): - raise ValidationError("Failed to verify the keystores.") - if not verify_deposit_data_json(deposits_file, credentials.credentials): - raise ValidationError("Failed to verify the deposit data JSON files.") - -def parse_create_mnemonic(args): - """Parse CLI arguments to call the create_mnemonic function. - """ - mnemonic = None - if args.language: - mnemonic = create_mnemonic(args.wordlist, language=args.language) - else: - mnemonic = create_mnemonic(args.wordlist) - - print(json.dumps({ - 'mnemonic': mnemonic - })) - -def parse_generate_keys(args): - generate_keys(args) - -def parse_validate_mnemonic(args): - """Parse CLI arguments to call the validate_mnemonic function. - """ - validate_mnemonic(args.mnemonic, args.wordlist) - -def main(): - """The application starting point. - """ - main_parser = argparse.ArgumentParser() - - subparsers = main_parser.add_subparsers(title="subcommands") - - create_parser = subparsers.add_parser("create_mnemonic") - create_parser.add_argument("wordlist", help="Path to word list directory", type=str) - create_parser.add_argument("--language", help="Language", type=str) - create_parser.set_defaults(func=parse_create_mnemonic) - - generate_parser = subparsers.add_parser("generate_keys") - generate_parser.add_argument("wordlist", help="Path to word list directory", type=str) - generate_parser.add_argument("mnemonic", help="Mnemonic", type=str) - generate_parser.add_argument("index", help="Validator start index", type=int) - generate_parser.add_argument("count", help="Validator count", type=int) - generate_parser.add_argument("folder", help="Where to put the deposit data and keystore files", type=str) - generate_parser.add_argument("network", help="For which network to create these keys for", type=str) - generate_parser.add_argument("password", help="Password for the keystore files", type=str) - generate_parser.add_argument("--eth1_withdrawal_address", help="Optional eth1 withdrawal address", type=str) - generate_parser.set_defaults(func=parse_generate_keys) - - validate_parser = subparsers.add_parser("validate_mnemonic") - validate_parser.add_argument("wordlist", help="Path to word list directory", type=str) - validate_parser.add_argument("mnemonic", help="Mnemonic", type=str) - validate_parser.set_defaults(func=parse_validate_mnemonic) - - args = main_parser.parse_args() - if not args or 'func' not in args: - main_parser.parse_args(['-h']) - else: - try: - args.func(args) - except (ValidationError, ValueError) as exc: - print(str(exc), file=sys.stderr) - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/scripts/stakingdeposit_proxy.py b/src/scripts/stakingdeposit_proxy.py new file mode 100644 index 00000000..cf187541 --- /dev/null +++ b/src/scripts/stakingdeposit_proxy.py @@ -0,0 +1,375 @@ +"""The stakingdeposit_proxy application. + +This application is used as a proxy between our electron application and the staking-deposit-cli +internals. It exposes some staking-deposit-cli functions as easy to use commands that can be called +on the CLI. +""" + +import os +import argparse +import json +import sys +import time +from typing import ( + Sequence, +) + +from eth_typing import HexAddress + +from staking_deposit.key_handling.key_derivation.mnemonic import ( + get_mnemonic, + reconstruct_mnemonic +) + +from eth_utils import is_hex_address, to_normalized_address + +from staking_deposit.credentials import ( + CredentialList, + Credential +) + +from staking_deposit.exceptions import ValidationError +from staking_deposit.utils.validation import ( + validate_deposit, + validate_bls_to_execution_change +) +from staking_deposit.utils.constants import ( + MAX_DEPOSIT_AMOUNT, +) + +from staking_deposit.settings import ( + get_chain_setting, +) + +from staking_deposit.utils.crypto import SHA256 + +def generate_bls_to_execution_change( + folder: str, + chain: str, + mnemonic: str, + validator_start_index: int, + validator_indices: Sequence[int], + bls_withdrawal_credentials_list: Sequence[bytes], + execution_address: HexAddress + ) -> None: + """Generate bls to execution change file. + + Keyword arguments: + folder -- folder path for the resulting bls to execution change files + chain -- chain setting for the signing domain, possible values are 'mainnet', + 'goerli', 'zhejiang' + mnemonic -- mnemonic to be used as the seed for generating the keys + validator_start_index -- index position for the keys to start generating withdrawal credentials + validator_indices -- a list of the chosen validator index number(s) as identified on the beacon chain + bls_withdrawal_credentials_list -- a list of the old BLS withdrawal credentials of the given validator(s) + execution_address -- withdrawal address + """ + if not os.path.exists(folder): + os.mkdir(folder) + + eth1_withdrawal_address = execution_address + if not is_hex_address(eth1_withdrawal_address): + raise ValueError("The given Eth1 address is not in hexadecimal encoded form.") + + eth1_withdrawal_address = to_normalized_address(eth1_withdrawal_address) + execution_address = eth1_withdrawal_address + + # Get chain setting + chain_setting = get_chain_setting(chain) + + if len(validator_indices) != len(bls_withdrawal_credentials_list): + raise ValueError( + "The size of `validator_indices` (%d) should be as same as `bls_withdrawal_credentials_list` (%d)." + % (len(validator_indices), len(bls_withdrawal_credentials_list)) + ) + + num_validators = len(validator_indices) + amounts = [MAX_DEPOSIT_AMOUNT] * num_validators + + mnemonic_password = '' + + num_keys = num_validators + start_index = validator_start_index + hex_eth1_withdrawal_address = execution_address + + if len(amounts) != num_keys: + raise ValueError( + f"The number of keys ({num_keys}) doesn't equal to the corresponding deposit amounts ({len(amounts)})." + ) + key_indices = range(start_index, start_index + num_keys) + credentials = CredentialList( + [Credential(mnemonic=mnemonic, mnemonic_password=mnemonic_password, + index=index, amount=amounts[index - start_index], chain_setting=chain_setting, + hex_eth1_withdrawal_address=hex_eth1_withdrawal_address) + for index in key_indices]) + + # Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated + for i, credential in enumerate(credentials.credentials): + bls_withdrawal_credentials = bls_withdrawal_credentials_list[i] + if bls_withdrawal_credentials[1:] != SHA256(credential.withdrawal_pk)[1:]: + raise ValidationError('err_not_matching') + + bls_to_execution_changes = [cred.get_bls_to_execution_change_dict(validator_indices[i]) + for i, cred in enumerate(credentials.credentials)] + + filefolder = os.path.join(folder, 'bls_to_execution_change-%i.json' % time.time()) + with open(filefolder, 'w') as f: + json.dump(bls_to_execution_changes, f) + if os.name == 'posix': + os.chmod(filefolder, int('440', 8)) # Read for owner & group + btec_file = filefolder + + with open(btec_file, 'r') as f: + btec_json = json.load(f) + json_file_validation_result = all([ + validate_bls_to_execution_change( + btec, credential, + input_validator_index=input_validator_index, + input_execution_address=execution_address, + chain_setting=chain_setting) + for btec, credential, input_validator_index in zip(btec_json, credentials.credentials, validator_indices) + ]) + if not json_file_validation_result: + raise ValidationError('err_verify_btec') + +def validate_bls_credentials( + chain: str, + mnemonic: str, + validator_start_index: int, + bls_withdrawal_credentials_list: Sequence[bytes], + ) -> None: + """Validate BLS credentials against what was generated from a mnemonic. + + Keyword arguments: + chain -- chain setting for the signing domain, possible values are 'mainnet', + 'goerli', 'zhejiang' + mnemonic -- mnemonic to be used as the seed for generating the keys + validator_start_index -- index position for the keys to start generating withdrawal credentials + bls_withdrawal_credentials_list -- a list of the old BLS withdrawal credentials of the given validator(s) + """ + + chain_setting = get_chain_setting(chain) + + num_validators = len(bls_withdrawal_credentials_list) + amounts = [MAX_DEPOSIT_AMOUNT] * num_validators + + mnemonic_password = '' + + num_keys = num_validators + start_index = validator_start_index + + key_indices = range(start_index, start_index + num_keys) + credentials = CredentialList( + [Credential(mnemonic=mnemonic, mnemonic_password=mnemonic_password, + index=index, amount=amounts[index - start_index], chain_setting=chain_setting, + hex_eth1_withdrawal_address=None) + for index in key_indices]) + + # Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated + for i, credential in enumerate(credentials.credentials): + bls_withdrawal_credentials = bls_withdrawal_credentials_list[i] + if bls_withdrawal_credentials[1:] != SHA256(credential.withdrawal_pk)[1:]: + raise ValidationError('err_not_matching') + + +def validate_mnemonic(mnemonic: str, word_lists_path: str) -> str: + """Validate a mnemonic using the staking-deposit-cli logic and returns the mnemonic. + + Keyword arguments: + mnemonic -- the mnemonic to validate + word_lists_path -- path to the word lists directory + """ + + mnemonic = reconstruct_mnemonic(mnemonic, word_lists_path) + if mnemonic is not None: + return mnemonic + else: + raise ValidationError('That is not a valid mnemonic, please check for typos.') + +def create_mnemonic(word_list, language='english'): + """Returns a new random mnemonic. + + Keyword arguments: + word_lists -- path to the word lists directory + language -- the language for the mnemonic words, possible values are 'chinese_simplified', + 'chinese_traditional', 'czech', 'english', 'italian', 'korean', 'portuguese' or + 'spanish' (default 'english') + """ + return get_mnemonic(language=language, words_path=word_list) + +def generate_keys(args): + """Generate validator keys. + + Keyword arguments: + args -- contains the CLI arguments pass to the application, it should have those properties: + - wordlist: path to the word lists directory + - mnemonic: mnemonic to be used as the seed for generating the keys + - index: index of the first validator's keys you wish to generate + - count: number of signing keys you want to generate + - folder: folder path for the resulting keystore(s) and deposit(s) files + - network: network setting for the signing domain, possible values are 'mainnet', + 'prater', 'kintsugi' or 'kiln' + - password: password that will protect the resulting keystore(s) + - eth1_withdrawal_address: (Optional) eth1 address that will be used to create the + withdrawal credentials + """ + + eth1_withdrawal_address = None + if args.eth1_withdrawal_address: + eth1_withdrawal_address = args.eth1_withdrawal_address + if not is_hex_address(eth1_withdrawal_address): + raise ValueError("The given Eth1 address is not in hexadecimal encoded form.") + + eth1_withdrawal_address = to_normalized_address(eth1_withdrawal_address) + + mnemonic = validate_mnemonic(args.mnemonic, args.wordlist) + mnemonic_password = '' + amounts = [MAX_DEPOSIT_AMOUNT] * args.count + folder = args.folder + chain_setting = get_chain_setting(args.network) + if not os.path.exists(folder): + os.mkdir(folder) + + start_index = args.index + num_keys=args.count + hex_eth1_withdrawal_address=eth1_withdrawal_address + password=args.password + + if len(amounts) != num_keys: + raise ValueError( + f"The number of keys ({num_keys}) doesn't equal to the corresponding deposit amounts ({len(amounts)})." + ) + key_indices = range(start_index, start_index + num_keys) + credentials = CredentialList( + [Credential(mnemonic=mnemonic, mnemonic_password=mnemonic_password, + index=index, amount=amounts[index - start_index], chain_setting=chain_setting, + hex_eth1_withdrawal_address=hex_eth1_withdrawal_address) + for index in key_indices]) + + keystore_filefolders = [credential.save_signing_keystore(password=password, folder=folder) for credential in credentials.credentials] + + deposit_data = [cred.deposit_datum_dict for cred in credentials.credentials] + filefolder = os.path.join(folder, 'deposit_data-%i.json' % time.time()) + with open(filefolder, 'w') as f: + json.dump(deposit_data, f, default=lambda x: x.hex()) + if os.name == 'posix': + os.chmod(filefolder, int('440', 8)) # Read for owner & group + deposits_file = filefolder + + items = zip(credentials.credentials, keystore_filefolders) + + if not all(credential.verify_keystore(keystore_filefolder=filefolder, password=password) + for credential, filefolder in items): + raise ValidationError("Failed to verify the keystores.") + + with open(deposits_file, 'r') as f: + deposit_json = json.load(f) + if not all([validate_deposit(deposit, credential) for deposit, credential in zip(deposit_json, credentials.credentials)]): + raise ValidationError("Failed to verify the deposit data JSON files.") + +def decode_bytes(value): + if value.startswith('0x'): + value = value[2:] + return bytes.fromhex(value) + +def parse_bls_change(args): + """Parse CLI arguments to call the generate_bls_to_execution_change function. + """ + generate_bls_to_execution_change( + args.folder, + args.chain, + args.mnemonic, + args.index, + [int(i) for i in args.indices.split(',')], + [decode_bytes(i) for i in args.withdrawal_credentials.split(',')], + args.execution_address) + +def parse_validate_bls_credentials(args): + """Parse CLI arguments to call the validate_bls_credentials function. + """ + validate_bls_credentials( + args.chain, + args.mnemonic, + args.index, + [decode_bytes(i) for i in args.withdrawal_credentials.split(',')]) + +def parse_create_mnemonic(args): + """Parse CLI arguments to call the create_mnemonic function. + """ + mnemonic = None + if args.language: + mnemonic = create_mnemonic(args.wordlist, language=args.language) + else: + mnemonic = create_mnemonic(args.wordlist) + + print(json.dumps({ + 'mnemonic': mnemonic + })) + +def parse_generate_keys(args): + """Parse CLI arguments to call the generate_keys function. + """ + generate_keys(args) + +def parse_validate_mnemonic(args): + """Parse CLI arguments to call the validate_mnemonic function. + """ + validate_mnemonic(args.mnemonic, args.wordlist) + +def main(): + """The application starting point. + """ + main_parser = argparse.ArgumentParser() + + subparsers = main_parser.add_subparsers(title="subcommands") + + create_parser = subparsers.add_parser("create_mnemonic") + create_parser.add_argument("wordlist", help="Path to word list directory", type=str) + create_parser.add_argument("--language", help="Language", type=str) + create_parser.set_defaults(func=parse_create_mnemonic) + + generate_parser = subparsers.add_parser("generate_keys") + generate_parser.add_argument("wordlist", help="Path to word list directory", type=str) + generate_parser.add_argument("mnemonic", help="Mnemonic", type=str) + generate_parser.add_argument("index", help="Validator start index", type=int) + generate_parser.add_argument("count", help="Validator count", type=int) + generate_parser.add_argument("folder", help="Where to put the deposit data and keystore files", type=str) + generate_parser.add_argument("network", help="For which network to create these keys for", type=str) + generate_parser.add_argument("password", help="Password for the keystore files", type=str) + generate_parser.add_argument("--eth1_withdrawal_address", help="Optional eth1 withdrawal address", type=str) + generate_parser.set_defaults(func=parse_generate_keys) + + validate_parser = subparsers.add_parser("validate_mnemonic") + validate_parser.add_argument("wordlist", help="Path to word list directory", type=str) + validate_parser.add_argument("mnemonic", help="Mnemonic", type=str) + validate_parser.set_defaults(func=parse_validate_mnemonic) + + generate_parser = subparsers.add_parser("bls_change") + generate_parser.add_argument("folder", help="Where to put the bls change files", type=str) + generate_parser.add_argument("chain", help="For which network to create the change", type=str) + generate_parser.add_argument("mnemonic", help="Mnemonic", type=str) + generate_parser.add_argument("index", help="Validator start index", type=int) + generate_parser.add_argument("indices", help="Validator index number(s) as identified on the beacon chain (comma seperated)", type=str) + generate_parser.add_argument("withdrawal_credentials", help="Old BLS withdrawal credentials of the given validator(s) (comma seperated)", type=str) + generate_parser.add_argument("execution_address", help="withdrawal address", type=str) + generate_parser.set_defaults(func=parse_bls_change) + + generate_parser = subparsers.add_parser("validate_bls_credentials") + generate_parser.add_argument("chain", help="For which network to validate these BLS credentials", type=str) + generate_parser.add_argument("mnemonic", help="Mnemonic", type=str) + generate_parser.add_argument("index", help="Validator start index", type=int) + generate_parser.add_argument("withdrawal_credentials", help="Old BLS withdrawal credentials of the given validator(s) (comma seperated)", type=str) + generate_parser.set_defaults(func=parse_validate_bls_credentials) + + args = main_parser.parse_args() + if not args or 'func' not in args: + main_parser.parse_args(['-h']) + else: + try: + args.func(args) + except (ValidationError, ValueError) as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/vendors/tools-key-gen-cli b/src/vendors/tools-key-gen-cli index 2b4dcb10..377cf038 160000 --- a/src/vendors/tools-key-gen-cli +++ b/src/vendors/tools-key-gen-cli @@ -1 +1 @@ -Subproject commit 2b4dcb102d60338904a9e7e6f3ff084617d3ff2d +Subproject commit 377cf038d4b0919bf6bbaef02f584b616a8df7da