Skip to content

Commit

Permalink
feat: clean up SSZ lib
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Feb 7, 2025
1 parent 3bb45e8 commit 500e8be
Show file tree
Hide file tree
Showing 7 changed files with 611 additions and 287 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)
}
}
}
49 changes: 2 additions & 47 deletions contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ abstract contract CLProofVerifier {
function _validatePubKeyWCProof(ValidatorWitness calldata _witness, bytes32 _withdrawalCredentials) internal view {
// parent node for first two leaves in validator container tree
// pubkey + wc
bytes32 _leaf = _sha256Pair(_pubkeyRoot(_witness.pubkey), _withdrawalCredentials);
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));

Expand All @@ -45,8 +45,7 @@ abstract contract CLProofVerifier {
});
}

// virtual for testing
function _getParentBlockRoot(uint64 blockTimestamp) internal view virtual returns (bytes32) {
function _getParentBlockRoot(uint64 blockTimestamp) internal view returns (bytes32) {
(bool success, bytes memory data) = BEACON_ROOTS.staticcall(abi.encode(blockTimestamp));

if (!success || data.length == 0) {
Expand All @@ -60,50 +59,6 @@ abstract contract CLProofVerifier {
return GI_FIRST_VALIDATOR.shr(offset);
}

// hashes calldata validator pubkey
function _pubkeyRoot(bytes calldata pubkey) public 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)
}
}

// 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)
}
}

// proving errors
error InvalidGeneralIndex(uint256);
error RootNotFound();
error InvalidPubkeyLength();
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ contract PredepositGuarantee is CLProofVerifier {
if (bond.locked != 0) revert BondMustBeFullyUnlocked();

if (bond.total > 0 && nodeOperatorVoucher[msg.sender] != address(0)) {
uint256 ejected = nodeOperatorBonds[msg.sender].total;
nodeOperatorBonds[msg.sender].total = 0;
(bool success, ) = nodeOperatorVoucher[msg.sender].call{value: uint256(bond.total)}("");
(bool success, ) = nodeOperatorVoucher[msg.sender].call{value: ejected}("");

// voucher can block change?
if (!success) revert WithdrawalFailed();
Expand Down
Loading

0 comments on commit 500e8be

Please sign in to comment.