import fs from "node:fs"; import path from "node:path"; import {Keystore} from "@chainsafe/bls-keystore"; import {SecretKey} from "@chainsafe/blst"; import {fromHex, toHex, toPubkeyHex} from "@lodestar/utils"; import {SignerLocal, SignerType} from "@lodestar/validator"; import {writeFile600Perm} from "../../../util/file.js"; import {lockFilepath, unlockFilepath} from "../../../util/lockfile.js"; import {LocalKeystoreDefinition} from "./interface.js"; export async function loadKeystoreCache( cacheFilepath: string, keystoreDefinitions: LocalKeystoreDefinition[] ): Promise { const keystores: Keystore[] = []; const passwords: string[] = []; for (const {keystorePath, password} of keystoreDefinitions) { keystores.push(Keystore.parse(fs.readFileSync(keystorePath, "utf8"))); passwords.push(password); } if (keystores.length !== passwords.length) { throw new Error( `Number of keystores and passwords must be equal. keystores=${keystores.length}, passwords=${passwords.length}` ); } if (!fs.existsSync(cacheFilepath)) { throw new Error(`Cache file ${cacheFilepath} does not exists.`); } lockFilepath(cacheFilepath); const password = passwords.join(""); // We can't use Keystore.parse as it validates the `encrypted message` to be only 32 bytes. const keystore = new Keystore(JSON.parse(fs.readFileSync(cacheFilepath, "utf8"))); const secretKeyConcatenatedBytes = await keystore.decrypt(password); const result: SignerLocal[] = []; for (const [index, k] of keystores.entries()) { const secretKeyBytes = Uint8Array.prototype.slice.call(secretKeyConcatenatedBytes, index * 32, (index + 1) * 32); const secretKey = SecretKey.fromBytes(secretKeyBytes); const publicKey = secretKey.toPublicKey().toBytes(); if (toPubkeyHex(publicKey) !== toPubkeyHex(fromHex(k.pubkey))) { throw new Error( `Keystore ${k.uuid} does not match the expected pubkey. expected=${toPubkeyHex(fromHex(k.pubkey))}, found=${toHex( publicKey )}` ); } result.push({ type: SignerType.Local, secretKey, }); } unlockFilepath(cacheFilepath); return result; } export async function writeKeystoreCache( cacheFilepath: string, signers: SignerLocal[], passwords: string[] ): Promise { if (signers.length !== passwords.length) { throw new Error( `Number of signers and passwords must be equal. signers=${signers.length}, passwords=${passwords.length}` ); } const secretKeys = signers.map((s) => s.secretKey.toBytes()); const publicKeys = signers.map((s) => s.secretKey.toPublicKey().toBytes()); const password = passwords.join(""); const secretKeyConcatenatedBytes = Buffer.concat(secretKeys); const publicConcatenatedBytes = Buffer.concat(publicKeys); const keystore = await Keystore.create(password, secretKeyConcatenatedBytes, publicConcatenatedBytes, cacheFilepath); if (!fs.existsSync(path.dirname(cacheFilepath))) fs.mkdirSync(path.dirname(cacheFilepath), {recursive: true}); lockFilepath(cacheFilepath); writeFile600Perm(cacheFilepath, keystore.stringify()); unlockFilepath(cacheFilepath); } export async function clearKeystoreCache(cacheFilepath: string): Promise { if (fs.existsSync(cacheFilepath)) { unlockFilepath(cacheFilepath); fs.unlinkSync(cacheFilepath); } }