Create Validator Keys
The SSV SDK does not natively support programatically generating keystores out of the box, but a number of external libraries can be used to achieve this.
If you do not need to do this programatically, it may be easier to use the Ethereum Staking Deposit CLI to generate validator keys.
As this is not part of the SSV SDK, SSV is not liable for any misuse of the packages used to generate validator keys in the method show below.
Start by installing the packages that are needed to run the script below.
npm i viem @chainsafe/bls-keygen @chainsafe/bls-keystore @chainsafe/bls @chainsafe/ssz @lodestar/config @lodestar/params @lodestar/state-transition @lodestar/types abitype dotenv
Then the createValidatorKeys
function will return the keystores, deposit data, and secret key.
import type { SupportedChains, chains } from '@/config/chains'
import { deriveEth2ValidatorKeys, generateRandomSecretKey } from '@chainsafe/bls-keygen'
import { create } from '@chainsafe/bls-keystore'
import bls from '@chainsafe/bls/herumi'
import { fromHexString, toHexString } from '@chainsafe/ssz'
import type { ChainConfig } from '@lodestar/config'
import { holeskyChainConfig, mainnetChainConfig } from '@lodestar/config/networks'
import { DOMAIN_DEPOSIT } from '@lodestar/params'
import { ZERO_HASH, computeDomain, computeSigningRoot } from '@lodestar/state-transition'
import { ssz } from '@lodestar/types/phase0'
import type { Address } from 'abitype'
import type { Hex } from 'viem'
import { sha256, toBytes, toHex } from 'viem'
const chainConfigs: Record<keyof typeof chains, ChainConfig> = {
mainnet: mainnetChainConfig,
holesky: holeskyChainConfig,
}
type ValidatorKeysArgs = {
index?: number
count: number
chain: SupportedChains
withdrawal: Address
password: string
masterSK?: Uint8Array
}
export type DepositData = {
pubkey: string
withdrawal_credentials: string
amount: number
signature: string
deposit_message_root: string
deposit_data_root: string
fork_version: string
network_name: 'mainnet' | 'holesky'
}
// Add this verification function
function verifyDepositRoot(
pubkey: Hex,
withdrawalCredentials: Hex,
amount: number,
signature: Hex,
expectedRoot: Hex,
): boolean {
// Pad pubkey with 16 zero bytes
const pubkeyPadded = toBytes(pubkey)
const padding16 = new Uint8Array(16)
const pubkeyRoot = sha256(new Uint8Array([...pubkeyPadded, ...padding16]), 'bytes')
// Split and pad signature
const signatureBytes = toBytes(signature)
const signaturePart1 = signatureBytes.slice(0, 64)
const signaturePart2 = new Uint8Array([...signatureBytes.slice(64), ...new Uint8Array(32)])
const signatureRoot = sha256(
new Uint8Array([
...new Uint8Array(sha256(signaturePart1, 'bytes')),
...new Uint8Array(sha256(signaturePart2, 'bytes')),
]),
'bytes',
)
// Pack amount with 24 zero bytes
const amountBytes = new Uint8Array(8)
new DataView(amountBytes.buffer).setBigUint64(0, BigInt(amount), true)
const amountPadded = new Uint8Array([...amountBytes, ...new Uint8Array(24)])
const node = sha256(
new Uint8Array([
...sha256(new Uint8Array([...pubkeyRoot, ...toBytes(withdrawalCredentials)]), 'bytes'),
...sha256(new Uint8Array([...amountPadded, ...signatureRoot]), 'bytes'),
]),
'bytes',
)
return toHex(node) === expectedRoot
}
export async function createValidatorKeys({
index = 0,
count,
chain,
withdrawal,
password,
masterSK = generateRandomSecretKey(),
}: ValidatorKeysArgs) {
const keystores = []
const deposit_data = []
const chainConfig = chainConfigs[chain]
for (let i = index; i < count; i++) {
const sk = bls.SecretKey.fromBytes(deriveEth2ValidatorKeys(masterSK, i).signing)
const pubkey = sk.toPublicKey()
const pubkeyBytes = pubkey.toBytes()
const keystore = await create(password, sk.toBytes(), pubkeyBytes, `m/12381/3600/${i}/0/0`)
keystores.push(keystore)
// Generate deposit data
const withdrawalCredentials = fromHexString(
'0x010000000000000000000000' + withdrawal.replace('0x', ''),
)
const depositMessage = {
pubkey: pubkeyBytes,
withdrawalCredentials,
amount: 32e9,
}
const domain = computeDomain(DOMAIN_DEPOSIT, chainConfig.GENESIS_FORK_VERSION, ZERO_HASH)
const signingRoot = computeSigningRoot(ssz.DepositMessage, depositMessage, domain)
const depositData = {
...depositMessage,
signature: sk.sign(signingRoot).toBytes(),
}
const depositDataRoot = ssz.DepositData.hashTreeRoot(depositData)
const generated = {
pubkey: toHexString(pubkey.toBytes()).replace('0x', ''),
withdrawal_credentials: toHexString(withdrawalCredentials).replace('0x', ''),
amount: 32000000000,
signature: toHexString(depositData.signature).replace('0x', ''),
deposit_message_root: toHexString(signingRoot).replace('0x', ''),
deposit_data_root: toHexString(depositDataRoot).replace('0x', ''),
fork_version: toHexString(chainConfig.GENESIS_FORK_VERSION).replace('0x', ''),
network_name: chain,
}
// Add verification before pushing to deposit_data
const isValid = verifyDepositRoot(
`0x${generated.pubkey}`,
`0x${generated.withdrawal_credentials}`,
generated.amount,
`0x${generated.signature}`,
`0x${generated.deposit_data_root}`,
)
if (!isValid) {
throw new Error(`Generated deposit data verification failed for validator ${i}`)
}
deposit_data.push(generated)
}
return {
keystores,
deposit_data,
masterSK,
}
}
Last updated