// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 /* SSZ library from CSM original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol */ // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity ^0.8.25; import {BeaconBlockHeader, Validator} from "./BeaconTypes.sol"; import {GIndex} from "./GIndex.sol"; library SSZ { error BranchHasMissingItem(); error BranchHasExtraItem(); error InvalidProof(); function hashTreeRoot(BeaconBlockHeader memory header) internal view returns (bytes32 root) { bytes32[8] memory nodes = [ toLittleEndian(header.slot), toLittleEndian(header.proposerIndex), header.parentRoot, header.stateRoot, header.bodyRoot, bytes32(0), bytes32(0), bytes32(0) ]; /// @solidity memory-safe-assembly assembly { // Count of nodes to hash let count := 8 // Loop over levels // prettier-ignore for { } 1 { } { // Loop over nodes at the given depth // Initialize `offset` to the offset of `proof` elements in memory. let target := nodes let source := nodes let end := add(source, shl(5, count)) // prettier-ignore for { } 1 { } { // Read next two hashes to hash mcopy(0x00, source, 0x40) // Call sha256 precompile let result := staticcall( gas(), 0x02, 0x00, 0x40, 0x00, 0x20 ) if iszero(result) { // Precompiles returns no data on OutOfGas error. revert(0, 0) } // Store the resulting hash at the target location mstore(target, mload(0x00)) // Advance the pointers target := add(target, 0x20) source := add(source, 0x40) if iszero(lt(source, end)) { break } } count := shr(1, count) if eq(count, 1) { root := mload(0x00) break } } } } function hashTreeRoot(Validator memory validator) internal view returns (bytes32 root) { bytes32 pubkeyRoot; assembly { // Dynamic data types such as bytes are stored at the specified offset. let offset := mload(validator) // Copy the pubkey to the scratch space. mcopy(0x00, add(offset, 32), 48) // Clear the last 16 bytes. mcopy(48, 0x60, 16) // Call sha256 precompile. let result := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20) if iszero(result) { // Precompiles returns no data on OutOfGas error. revert(0, 0) } pubkeyRoot := mload(0x00) } bytes32[8] memory nodes = [ pubkeyRoot, validator.withdrawalCredentials, toLittleEndian(validator.effectiveBalance), toLittleEndian(validator.slashed), toLittleEndian(validator.activationEligibilityEpoch), toLittleEndian(validator.activationEpoch), toLittleEndian(validator.exitEpoch), toLittleEndian(validator.withdrawableEpoch) ]; /// @solidity memory-safe-assembly assembly { // Count of nodes to hash let count := 8 // Loop over levels // prettier-ignore for { } 1 { } { // Loop over nodes at the given depth // Initialize `offset` to the offset of `proof` elements in memory. let target := nodes let source := nodes let end := add(source, shl(5, count)) // prettier-ignore for { } 1 { } { // Read next two hashes to hash mcopy(0x00, source, 0x40) // Call sha256 precompile let result := staticcall( gas(), 0x02, 0x00, 0x40, 0x00, 0x20 ) if iszero(result) { // Precompiles returns no data on OutOfGas error. revert(0, 0) } // Store the resulting hash at the target location mstore(target, mload(0x00)) // Advance the pointers target := add(target, 0x20) source := add(source, 0x40) if iszero(lt(source, end)) { break } } count := shr(1, count) if eq(count, 1) { root := mload(0x00) break } } } } /// @notice Modified version of `verify` from Solady `MerkleProofLib` to support generalized indices and sha256 precompile. /// @dev Reverts if `leaf` doesn't exist in the Merkle tree with `root`, given `proof`. function verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf, GIndex gI) internal view { uint256 index = gI.index(); /// @solidity memory-safe-assembly assembly { // Check if `proof` is empty. if iszero(proof.length) { // revert InvalidProof() mstore(0x00, 0x09bde339) revert(0x1c, 0x04) } // Left shift by 5 is equivalent to multiplying by 0x20. let end := add(proof.offset, shl(5, proof.length)) // Initialize `offset` to the offset of `proof` in the calldata. let offset := proof.offset // Iterate over proof elements to compute root hash. // prettier-ignore for { } 1 { } { // Slot of `leaf` in scratch space. // If the condition is true: 0x20, otherwise: 0x00. let scratch := shl(5, and(index, 1)) index := shr(1, index) if iszero(index) { // revert BranchHasExtraItem() mstore(0x00, 0x5849603f) // 0x1c = 28 => offset in 32-byte word of a slot 0x00 revert(0x1c, 0x04) } // Store elements to hash contiguously in scratch space. // Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes. mstore(scratch, leaf) // load next proof from calldata to the scratch space at 0x00 or 0x20 // xor() acts as if mstore(xor(scratch, 0x20), calldataload(offset)) // Call sha256 precompile. let result := staticcall( gas(), 0x02, // SHA-256 precompile 0x00, // input from scratch space from 0x00 0x40, // length is 2 leafs of 32 bytes each 0x00, // output back to scratch space at 0x00 0x20 // length of the output is 32 bytes ) if iszero(result) { // Precompile returns no data on OutOfGas error. revert(0, 0) } // Reuse `leaf` to store the hash to reduce stack operations. leaf := mload(0x00) offset := add(offset, 0x20) if iszero(lt(offset, end)) { break } } if iszero(eq(index, 1)) { // revert BranchHasMissingItem() mstore(0x00, 0x1b6661c3) revert(0x1c, 0x04) } if iszero(eq(leaf, root)) { // revert InvalidProof() mstore(0x00, 0x09bde339) revert(0x1c, 0x04) } } } // See https://github.com/succinctlabs/telepathy-contracts/blob/5aa4bb7/src/libraries/SimpleSerialize.sol#L17-L28 function toLittleEndian(uint256 v) internal pure returns (bytes32) { v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); v = ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) | ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); v = ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) | ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); v = (v >> 128) | (v << 128); return bytes32(v); } function toLittleEndian(bool v) internal pure returns (bytes32) { return bytes32(v ? 1 << 248 : 0); } }