Skip to content

Commit

Permalink
test: pdg cl verifier test
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Feb 4, 2025
1 parent 5212738 commit 0d5f663
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ contract CLProofVerifier {
return _validator.withdrawalCredentials;
}

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

if (!success || data.length == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,16 @@ contract PredepositGuarantee is CLProofVerifier {
function withdrawDisprovenCollateral(bytes calldata validatorPubkey, address _recipient) external {
if (_recipient == address(0)) revert ZeroArgument("_recipient");

if (_recipient == address(validatorStakingVault[validatorPubkey])) revert WithdrawToVaultNotAllowed();

if (validatorStatuses[validatorPubkey] != ValidatorStatus.PROVED_INVALID) revert ValidatorNotProvenInvalid();

if (msg.sender != validatorStakingVault[validatorPubkey].owner()) revert WithdrawSenderNotStakingVaultOwner();

validatorStatuses[validatorPubkey] = ValidatorStatus.WITHDRAWN;

(bool success, ) = _recipient.call{value: PREDEPOSIT_AMOUNT}("");

if (!success) revert WithdrawalFailed();

//TODO: events
Expand Down Expand Up @@ -233,6 +236,7 @@ contract PredepositGuarantee is CLProofVerifier {
error WithdrawValidatorDoesNotBelongToNodeOperator();
error WithdrawalCollateralOfWrongVault();
error WithdrawalCredentialsAreValid();
error WithdrawToVaultNotAllowed();
/// withdrawal generic
error WithdrawalFailed();

Expand Down
42 changes: 42 additions & 0 deletions test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { hexlify, randomBytes } from "ethers";
import { ethers } from "hardhat";

import { CLProofVerifier__Harness } from "typechain-types";
import { ValidatorStruct } from "typechain-types/contracts/0.8.25/predeposit_guarantee/PredepositGuarantee";

Check failure on line 5 in test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

Cannot find module 'typechain-types/contracts/0.8.25/predeposit_guarantee/PredepositGuarantee' or its corresponding type declarations.

import { ether } from "lib";

export const generateValidator = (customWC?: string, customPukey?: string): ValidatorStruct => {
const randomInt = (max: number): number => Math.floor(Math.random() * max);
const randomBytes32 = (): string => hexlify(randomBytes(32));
const randomValidatorPubkey = (): string => hexlify(randomBytes(96));

return {
pubkey: customPukey ?? randomValidatorPubkey(),
withdrawalCredentials: customWC ?? randomBytes32(),
effectiveBalance: ether(randomInt(32).toString()),
slashed: false,
activationEligibilityEpoch: randomInt(343300),
activationEpoch: randomInt(343300),
exitEpoch: randomInt(343300),
withdrawableEpoch: randomInt(343300),
};
};

// random number integer generator

describe("CLProofVerifier.sol", () => {
let CLProofVerifier: CLProofVerifier__Harness;
before(async () => {
CLProofVerifier = await ethers.deployContract("CLProofVerifier__Harness", {});
});

it("should verify validator object in merkle tree", async () => {
const validator = generateValidator();
await CLProofVerifier.TEST_addValidatorLeaf(validator);
const validator_index = await CLProofVerifier.TEST_lastIndex();
const proofs = await CLProofVerifier.TEST_getProof(validator_index);

await CLProofVerifier.TEST_validateWCProof(validator, proofs, 1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: UNLICENSED
// for testing purposes only

pragma solidity 0.8.25;

import {CLProofVerifier, Validator, SSZ} from "contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol";

import {MerkleProof} from "@openzeppelin/contracts-v5.2/utils/cryptography/MerkleProof.sol";

contract CLProofVerifier__Harness is CLProofVerifier {
// Store the leaves of the Merkle tree.
bytes32[] public leaves;

/**
* @notice Adds a new leaf to the tree.
* @param leaf The new leaf (for example, the keccak256 hash of some data).
* @return The index of the new leaf in the `leaves` array.
*/
function TEST_addLeaf(bytes32 leaf) public returns (uint256) {
leaves.push(leaf);
return leaves.length - 1;
}

function TEST_lastIndex() public view returns (uint256) {
return leaves.length - 1;
}

/**
* @notice Adds a new validator leaf to the tree.
* @param _validator the new validator container to add to merkle tree
* @return The index of the new leaf in the `leaves` array.
*/
function TEST_addValidatorLeaf(Validator calldata _validator) public returns (uint256) {
return TEST_addLeaf(SSZ.hashTreeRoot(_validator));
}

/**
* @notice Generates a Merkle proof for the leaf at the given index.
* The proof is an array of sibling hashes (one per level) using sorted pair hashing.
* @dev This implementation builds the tree on chain. For an odd number of nodes at any level,
* the last node is paired with itself.
* @param index The index of the target leaf in the `leaves` array.
* @return proof An array of sibling nodes that form the proof.
*/
function TEST_getProof(uint256 index) public view returns (bytes32[] memory proof) {
require(index < leaves.length, "Index out of bounds");

// Compute the number of levels in the tree.
// For a single leaf, the tree height is 1.
uint256 totalLevels = 1;
uint256 len = leaves.length;
while (len > 1) {
totalLevels++;
len = (len + 1) / 2;
}
// The proof will have one element per level except the root.
proof = new bytes32[](totalLevels - 1);

bytes32[] memory currentLevel = leaves;
uint256 proofIndex = 0;
uint256 currentIndex = index;

// Traverse up the tree until we reach the root.
while (currentLevel.length > 1) {
uint256 siblingIndex;
if (currentIndex % 2 == 0) {
siblingIndex = currentIndex + 1;
} else {
siblingIndex = currentIndex - 1;
}
// If the sibling exists, use it; otherwise, duplicate the current node.
if (siblingIndex < currentLevel.length) {
proof[proofIndex] = currentLevel[siblingIndex];
} else {
proof[proofIndex] = currentLevel[currentIndex];
}
proofIndex++;

// Build the next level using sorted pair hashing.
uint256 nextLevelLength = (currentLevel.length + 1) / 2;
bytes32[] memory nextLevel = new bytes32[](nextLevelLength);
for (uint256 i = 0; i < currentLevel.length; i += 2) {
if (i + 1 < currentLevel.length) {
nextLevel[i / 2] = _hashPair(currentLevel[i], currentLevel[i + 1]);
} else {
nextLevel[i / 2] = _hashPair(currentLevel[i], currentLevel[i]);
}
}
currentIndex = currentIndex / 2;
currentLevel = nextLevel;
}
}

/**
* @dev Internal function to compute the Merkle root from an array of leaves.
* Uses sorted pair hashing at each level. If the number of nodes is odd, the last node is paired with itself.
* @param _leaves The array of leaves.
* @return The Merkle tree root.
*/
function _computeMerkleRoot(bytes32[] memory _leaves) internal pure returns (bytes32) {
if (_leaves.length == 0) {
return bytes32(0);
}
bytes32[] memory currentLevel = _leaves;
while (currentLevel.length > 1) {
uint256 nextLevelLength = (currentLevel.length + 1) / 2;
bytes32[] memory nextLevel = new bytes32[](nextLevelLength);
for (uint256 i = 0; i < currentLevel.length; i += 2) {
if (i + 1 < currentLevel.length) {
nextLevel[i / 2] = _hashPair(currentLevel[i], currentLevel[i + 1]);
} else {
nextLevel[i / 2] = _hashPair(currentLevel[i], currentLevel[i]);
}
}
currentLevel = nextLevel;
}
return currentLevel[0];
}

/**
* @dev Internal function to hash a pair of nodes in sorted order.
* This is identical to the hashing used by OpenZeppelin's MerkleProof.
* @param a First hash.
* @param b Second hash.
* @return The hash of the sorted pair.
*/
function _hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) {
return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));
}

// override for
function _getParentBlockRoot(uint64) internal view override returns (bytes32) {
return _computeMerkleRoot(leaves);
}

function TEST_validateWCProof(
Validator calldata _validator,
bytes32[] calldata _proof,
uint64 beaconBlockTimestamp
) public view {
require(_validator.withdrawalCredentials == super._validateWCProof(_validator, _proof, beaconBlockTimestamp));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
describe("PredepositGuarantee.sol", () => {});

0 comments on commit 0d5f663

Please sign in to comment.