Skip to content

Commit

Permalink
Merge branch 'predeposit-guardian' of github.com:lidofinance/core int…
Browse files Browse the repository at this point in the history
…o predeposit-guardian-fixes
  • Loading branch information
DiRaiks committed Feb 10, 2025
2 parents ef47aed + 364e1e7 commit 04a70a9
Show file tree
Hide file tree
Showing 7 changed files with 749 additions and 296 deletions.
172 changes: 45 additions & 127 deletions contracts/0.8.25/lib/SSZ.sol
Original file line number Diff line number Diff line change
@@ -1,124 +1,20 @@
// SPDX-FileCopyrightText: 2024 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

/*
Cut version of SSZ library from CSM, only supports Validator container
original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol
*/

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {GIndex} from "./GIndex.sol";

// As defined in phase0/beacon-chain.md:356
struct Validator {
bytes pubkey;
bytes32 withdrawalCredentials;
uint64 effectiveBalance;
bool slashed;
uint64 activationEligibilityEpoch;
uint64 activationEpoch;
uint64 exitEpoch;
uint64 withdrawableEpoch;
}

/*
Cut and modified version of SSZ library from CSM only has methods for merkilized SSZ proof validation
original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol
*/
library SSZ {
error BranchHasMissingItem();
error BranchHasExtraItem();
error InvalidProof();

function hashTreeRoot(Validator calldata validator) internal view returns (bytes32 root) {
bytes32 pubkeyRoot;

assembly {
// In calldata, a dynamic field is encoded as an offset (relative to the start
// of the struct’s calldata) followed by its contents. The first 32 bytes of
// `validator` is the offset for `pubkey`. (Remember that `pubkey` is expected
// to be exactly 48 bytes long.)
let pubkeyOffset := calldataload(validator)
// The pubkey’s actual data is encoded at:
// validator + pubkeyOffset + 32
// because the first word at that location is the length.
// Copy 48 bytes of pubkey data into memory at 0x00.
calldatacopy(0x00, add(validator, add(pubkeyOffset, 32)), 48)
// Zero the remaining 16 bytes to form a 64‐byte block.
// (0x30 = 48, so mstore at 0x30 will zero 32 bytes covering addresses 48–79;
// only bytes 48–63 matter for our 64-byte input.)
mstore(0x30, 0)
// Call the SHA‑256 precompile (at address 0x02) with the 64-byte block.
if iszero(staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20)) {
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
}
}
}
}
error InvalidPubkeyLength();

/// @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`.
Expand Down Expand Up @@ -190,25 +86,47 @@ library SSZ {
}
}

// 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);
/// @notice Extracted part from `verifyProof` for hashing two leaves
/// @dev Combines 2 bytes32 in 64 bytes input for sha256 precompile
function sha256Pair(bytes32 left, bytes32 right) internal view returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
// Store `left` at memory position 0x00
mstore(0x00, left)
// Store `right` at memory position 0x20
mstore(0x20, right)

// Call SHA-256 precompile (0x02) with 64-byte input at memory 0x00
let success := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20)
if iszero(success) {
revert(0, 0)
}

// Load the resulting hash from memory
result := mload(0x00)
}
}

function toLittleEndian(bool v) internal pure returns (bytes32) {
return bytes32(v ? 1 << 248 : 0);
/// @notice Extracted and modified part from `hashTreeRoot` for hashing validator pubkey from calldata
/// @dev Reverts if `pubkey` length is not 48
function pubkeyRoot(bytes calldata pubkey) internal view returns (bytes32 _pubkeyRoot) {
if (pubkey.length != 48) revert InvalidPubkeyLength();

/// @solidity memory-safe-assembly
assembly {
// Copy 48 bytes of `pubkey` to memory at 0x00
calldatacopy(0x00, pubkey.offset, 48)

// Zero the remaining 16 bytes to form a 64-byte input block
mstore(0x30, 0)

// Call the SHA-256 precompile (0x02) with the 64-byte input
if iszero(staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20)) {
revert(0, 0)
}

// Load the resulting SHA-256 hash
_pubkeyRoot := mload(0x00)
}
}
}
88 changes: 66 additions & 22 deletions contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,79 @@
// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {Validator, SSZ, GIndex} from "contracts/0.8.25/lib/SSZ.sol";

struct ValidatorWitness {
Validator validator;
bytes32[] proof;
uint256 validatorIndex;
uint64 beaconBlockTimestamp;
}
import {GIndex, pack, concat} from "contracts/0.8.25/lib/GIndex.sol";
import {SSZ} from "contracts/0.8.25/lib/SSZ.sol";

/**
* @title CLProofVerifier
* @author Lido
* @notice
*
* CLProofVerifier is base abstract contract that provides internal method to verify
* merkle proofs of validator entry in CL. It uses concatenated proofs that prove
* validator existence in CL just from pubkey and withdrawalCredentials againts Beacon block root
* stored in BeaconRoots contract.
*
*
* NB!: GI_FIRST_VALIDATOR must be updated if Ethereum hardfork changes order of CL state tree
* (e.g. Pectra, Altair, etc.)
*
*/
abstract contract CLProofVerifier {
using SSZ for Validator;
struct ValidatorWitness {
bytes32[] proof;
bytes pubkey;
uint256 validatorIndex;
uint64 childBlockTimestamp;
}

// See `BEACON_ROOTS_ADDRESS` constant in the EIP-4788.
address public immutable BEACON_ROOTS;
address public immutable BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;

// Index of parent node for (Pubkey,WC) in validator container
GIndex public immutable GI_PUBKEY_WC_PARENT = pack(1 << 2, 2);
// Index of stateRoot in Beacon Block state
GIndex public immutable GI_STATE_VIEW = pack((1 << 3) + 3, 3);
// Index of first validator in CL state
GIndex public immutable GI_FIRST_VALIDATOR;

constructor(GIndex _gIFirstValidator) {
BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;
GI_FIRST_VALIDATOR = _gIFirstValidator;
}

function _validateWCProof(ValidatorWitness calldata _witness) internal view {
/**
* @notice validates proof of validator in CL with withdrawalCredentials and pubkey against Beacon block root
* @param _witness object containing user input passed as calldata
* `proof` - array of hashes for concatenated merkle proof from parent(pubkey,wc) node to the Beacon block root
* `pubkey` - pubkey of the validator
* `validatorIndex` - numerical index of validator in CL
* `childBlockTimestamp` - timestamp of EL block that has Beacon root corresponding to proof
* @param _withdrawalCredentials to verify proof with
* @dev reverts with `InvalidProof` when provided input cannot be proven to Beacon block root
*/
function _validatePubKeyWCProof(ValidatorWitness calldata _witness, bytes32 _withdrawalCredentials) internal view {
// parent node for first two leaves in validator container tree
// pubkey + wc
bytes32 _leaf = SSZ.sha256Pair(SSZ.pubkeyRoot(_witness.pubkey), _withdrawalCredentials);
// concatenated index for parent(pubkey + wc) -> Validator Index in state tree -> stateView Index in Beacon block Tree
GIndex _gIndex = concat(GI_STATE_VIEW, concat(_getValidatorGI(_witness.validatorIndex), GI_PUBKEY_WC_PARENT));

SSZ.verifyProof({
proof: _witness.proof,
root: _getParentBlockRoot(_witness.beaconBlockTimestamp),
leaf: _witness.validator.hashTreeRoot(),
gIndex: _getValidatorGI(_witness.validatorIndex)
root: _getParentBlockRoot(_witness.childBlockTimestamp),
leaf: _leaf,
gIndex: _gIndex
});
}

// virtual for testing
function _getParentBlockRoot(uint64 blockTimestamp) internal view virtual returns (bytes32) {
(bool success, bytes memory data) = BEACON_ROOTS.staticcall(abi.encode(blockTimestamp));
/**
* @notice returns parent CL block root for given child block timestamp
* @param _childBlockTimestamp timestamp of child block
* @return parent block root
* @dev reverts with `RootNotFound` if timestamp is not found in Beacon Block roots
*/
function _getParentBlockRoot(uint64 _childBlockTimestamp) internal view returns (bytes32) {
(bool success, bytes memory data) = BEACON_ROOTS.staticcall(abi.encode(_childBlockTimestamp));

if (!success || data.length == 0) {
revert RootNotFound();
Expand All @@ -44,11 +85,14 @@ abstract contract CLProofVerifier {
return abi.decode(data, (bytes32));
}

function _getValidatorGI(uint256 offset) internal view returns (GIndex) {
return GI_FIRST_VALIDATOR.shr(offset);
/**
* @notice calculates general validator index in CL state tree by provided offset
* @param _offset from first validator (Validator Index)
* @return gIndex of container in CL state tree
*/
function _getValidatorGI(uint256 _offset) internal view returns (GIndex) {
return GI_FIRST_VALIDATOR.shr(_offset);
}

// proving errors
error InvalidGeneralIndex(uint256);
error RootNotFound();
}
Loading

0 comments on commit 04a70a9

Please sign in to comment.