UNPKG

@exromany/lido-csm-sdk

Version:

[![GitHub license](https://img.shields.io/github/license/lidofinance/lido-csm-sdk?color=limegreen)](https://github.com/lidofinance/lido-csm-sdk/blob/main/LICENSE.txt) [![Version npm](https://img.shields.io/npm/v/@lidofinance/lido-csm-sdk?label=version)](h

186 lines 6.87 kB
/** * Alternative BLS signature verification implementation for Ethereum deposit data. * * This implementation follows the Ethereum consensus specification closely: * - Proper SSZ serialization using @chainsafe/ssz * - Clear separation of domain computation, signing root, and verification * - Better error handling and type safety * - Compatible with both @chainsafe/bls and bls-eth-wasm * * Reference: * - https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#bls-signatures * - https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/deposit-contract.md */ import { ByteVectorType, ContainerType, UintBigintType } from '@chainsafe/ssz'; import bls from 'bls-eth-wasm'; import { hexToBytes, toHex } from 'viem'; import { DOMAIN_DEPOSIT, FIXED_FORK_VERSION } from './constants.js'; /** * SSZ Types following Ethereum consensus spec */ /** DepositMessage as per consensus spec */ const DepositMessageSSZ = new ContainerType({ pubkey: new ByteVectorType(48), withdrawal_credentials: new ByteVectorType(32), amount: new UintBigintType(8), }); /** ForkData for computing fork-specific domain */ const ForkDataSSZ = new ContainerType({ current_version: new ByteVectorType(4), genesis_validators_root: new ByteVectorType(32), }); /** SigningData for computing signing root */ const SigningDataSSZ = new ContainerType({ object_root: new ByteVectorType(32), domain: new ByteVectorType(32), }); /** * BLS initialization */ let blsInitialized = false; const ensureBLSInit = async () => { if (!blsInitialized) { await bls.init(bls.BLS12_381); blsInitialized = true; } }; /** * Helper to ensure hex strings have 0x prefix */ const ensureHex = (value) => { return (value.startsWith('0x') ? value : `0x${value}`); }; /** * Get fork version for the chain * @throws {Error} If fork version is not found */ const getForkVersion = (chainId) => { const version = FIXED_FORK_VERSION[chainId]; if (!version) { throw new Error(`Fork version not found for chain ${chainId}`); } // Fork version is 4 bytes, pad if necessary const hex = version.padStart(8, '0'); return hexToBytes(ensureHex(hex)); }; /** * Compute fork data root using SSZ * * per consensus spec: * def compute_fork_data_root(current_version: Version, genesis_validators_root: Root) -> Root: * return hash_tree_root(ForkData( * current_version=current_version, * genesis_validators_root=genesis_validators_root, * )) */ const computeForkDataRoot = (currentVersion, genesisValidatorsRoot) => { const forkData = { current_version: currentVersion, genesis_validators_root: genesisValidatorsRoot, }; return ForkDataSSZ.hashTreeRoot(forkData); }; /** * Compute domain using domain type and fork data root * * per consensus spec: * def compute_domain(domain_type: DomainType, fork_version: Version=None, genesis_validators_root: Root=None) -> Domain: * if fork_version is None: * fork_version = config.GENESIS_FORK_VERSION * if genesis_validators_root is None: * genesis_validators_root = Root() * fork_data_root = compute_fork_data_root(fork_version, genesis_validators_root) * return Domain(domain_type + fork_data_root[:28]) */ const computeDomain = (domainType, forkVersion, genesisValidatorsRoot) => { const forkDataRoot = computeForkDataRoot(forkVersion, genesisValidatorsRoot); // Domain is domain_type (4 bytes) + first 28 bytes of fork_data_root const domain = new Uint8Array(32); domain.set(domainType, 0); domain.set(forkDataRoot.slice(0, 28), 4); return domain; }; /** * Compute signing root from object root and domain * * per consensus spec: * def compute_signing_root(ssz_object: SSZObject, domain: Domain) -> Root: * return hash_tree_root(SigningData( * object_root=hash_tree_root(ssz_object), * domain=domain, * )) */ const computeSigningRoot = (objectRoot, domain) => { const signingData = { object_root: objectRoot, domain: domain, }; return SigningDataSSZ.hashTreeRoot(signingData); }; /** * Compute deposit message root (hash tree root of DepositMessage) */ const computeDepositMessageRoot = (message) => { return DepositMessageSSZ.hashTreeRoot(message); }; /** * Verify BLS signature for deposit data following Ethereum consensus spec * * @param data - Deposit data to verify * @param chainId - Chain ID (mainnet or testnet) * @returns Promise<boolean> - True if signature is valid * * @example * const isValid = await verifyDepositSignature(depositData, CHAINS.Mainnet); * if (!isValid) { * console.error('Invalid signature'); * } */ export const verifyDepositSignature = async (data, chainId) => { try { // Initialize BLS library await ensureBLSInit(); // Parse input data (fields are already Hex type) const pubkey = hexToBytes(data.pubkey); const signature = hexToBytes(data.signature); const withdrawalCredentials = hexToBytes(data.withdrawal_credentials); const amount = BigInt(data.amount); // Validate input sizes if (pubkey.length !== 48 || signature.length !== 96 || withdrawalCredentials.length !== 32) { return false; } // Build DepositMessage const depositMessage = { pubkey, withdrawal_credentials: withdrawalCredentials, amount, }; // Step 1: Compute deposit message root const messageRoot = computeDepositMessageRoot(depositMessage); // Verify deposit_message_root matches if (toHex(messageRoot).toLowerCase() !== data.deposit_message_root.toLowerCase()) { return false; } // Step 2: Get chain-specific parameters const forkVersion = getForkVersion(chainId); // IMPORTANT: Per Ethereum spec, deposit signatures use ZERO genesis_validators_root // "The sole exception to the mixing-in of the fork version is signatures on deposits" // Reference: staking-deposit-cli uses ZERO_BYTES32 for deposits const genesisValidatorsRoot = new Uint8Array(32); // All zeros // Step 3: Compute domain const domainType = hexToBytes(DOMAIN_DEPOSIT); const domain = computeDomain(domainType, forkVersion, genesisValidatorsRoot); // Step 4: Compute signing root const signingRoot = computeSigningRoot(messageRoot, domain); // Step 5: Verify BLS signature const publicKey = new bls.PublicKey(); publicKey.deserialize(pubkey); const sig = new bls.Signature(); sig.deserialize(signature); return publicKey.verify(sig, signingRoot); } catch { return false; } }; //# sourceMappingURL=signature.js.map