Skip to content

Commit

Permalink
feat: rework for wc proof
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Feb 7, 2025
1 parent ecc06c6 commit 3bb45e8
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 41 deletions.
81 changes: 68 additions & 13 deletions contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,44 @@
// 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";

abstract contract CLProofVerifier {
using SSZ for Validator;
struct ValidatorWitness {
bytes32[] proof;
bytes pubkey;
uint256 validatorIndex;
uint64 parentBlockTimestamp;
}

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

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

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

function _validateWCProof(ValidatorWitness calldata _witness) internal view {
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);
// 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.parentBlockTimestamp),
leaf: _leaf,
gIndex: _gIndex
});
}

Expand All @@ -48,7 +60,50 @@ 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 @@ -4,7 +4,7 @@
// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {CLProofVerifier, ValidatorWitness, GIndex} from "./CLProofVerifier.sol";
import {CLProofVerifier, GIndex} from "./CLProofVerifier.sol";

import {IStakingVaultOwnable} from "../interfaces/IStakingVault.sol";

Expand Down Expand Up @@ -99,6 +99,11 @@ contract PredepositGuarantee is CLProofVerifier {
_topUpNodeOperatorCollateral(_nodeOperator);
}

// ensures vault fair play
if (address(_stakingVault) != _wcToAddress(_stakingVault.withdrawalCredentials())) {
revert stakingVaultWithdrawalCredentialsMismatch();
}

uint128 totalDepositAmount = PREDEPOSIT_AMOUNT * uint128(_deposits.length);

if (nodeOperatorBonds[_nodeOperator].total - nodeOperatorBonds[_nodeOperator].locked < totalDepositAmount)
Expand Down Expand Up @@ -126,8 +131,14 @@ contract PredepositGuarantee is CLProofVerifier {
// TODO: event
}

/*
*
* POSITIVE PROOF METHODS
*
*/

function proveValidatorWC(ValidatorWitness calldata _witness) external {
_processWCProof(_witness);
_processWitnessProof(_witness);
}

function depositToProvenValidators(
Expand Down Expand Up @@ -165,15 +176,43 @@ contract PredepositGuarantee is CLProofVerifier {
IStakingVaultOwnable _stakingVault
) external payable {
for (uint256 i = 0; i < _witnesses.length; i++) {
_processWCProof(_witnesses[i]);
_processWitnessProof(_witnesses[i]);
}

depositToProvenValidators(_stakingVault, _deposits);
}

/*
*
* NEGATIVE PROOF METHODS
*
*/

function proveInvalidValidatorWC(ValidatorWitness calldata _witness, bytes32 _invalidWithdrawalCredentials) public {
ValidatorStatus storage validatorStatus = validatorStatuses[_witness.pubkey];

if (validatorStatus.bondStatus != BondStatus.AWAITING_PROOF) {
revert ValidatorNotPreDeposited();
}

if (address(validatorStatus.stakingVault) == _wcToAddress(_invalidWithdrawalCredentials)) {
revert WithdrawalCredentialsAreValid();
}

_validatePubKeyWCProof(_witness, _invalidWithdrawalCredentials);

// reduces total&locked NO deposit
nodeOperatorBonds[validatorStatus.nodeOperator].total -= PREDEPOSIT_AMOUNT;
nodeOperatorBonds[validatorStatus.nodeOperator].locked -= PREDEPOSIT_AMOUNT;
// freed ether only will returned to owner of the vault with this validator
validatorStatus.bondStatus = BondStatus.PROVED_INVALID;

// TODO: events
}

// called by the staking vault owner if the predeposited validator was proven invalid
// i.e. node operator was malicious and has stolen vault ether
function withdrawDisprovenCollateral(bytes calldata validatorPubkey, address _recipient) external {
function withdrawDisprovenPredeposit(bytes calldata validatorPubkey, address _recipient) public {
ValidatorStatus storage validatorStatus = validatorStatuses[validatorPubkey];

if (_recipient == address(0)) revert ZeroArgument("_recipient");
Expand All @@ -193,6 +232,15 @@ contract PredepositGuarantee is CLProofVerifier {
//TODO: events
}

function disproveAndWithdraw(
ValidatorWitness calldata _witness,
bytes32 _invalidWithdrawalCredentials,
address _recipient
) external {
proveInvalidValidatorWC(_witness, _invalidWithdrawalCredentials);
withdrawDisprovenPredeposit(_witness.pubkey, _recipient);
}

/// Internal functions

function _validateNodeOperatorCaller(address _nodeOperator) internal view {
Expand All @@ -205,44 +253,36 @@ contract PredepositGuarantee is CLProofVerifier {
// TODO: event
}

function _wcToAddress(bytes32 _withdrawalCredentials) internal pure returns (address) {
return address(uint160(uint256(_withdrawalCredentials)));
}
function _wcToAddress(bytes32 _withdrawalCredentials) internal pure returns (address _wcAddress) {
uint64 _wcVersion = uint8(_withdrawalCredentials[0]);

if (_wcVersion < 1) {
revert WithdrawalCredentialsAreInvalid();
}

function _deconstructWC(bytes32 _withdrawalCredentials) internal pure returns (uint64, address) {
return (uint8(_withdrawalCredentials[0]), address(uint160(uint256(_withdrawalCredentials))));
_wcAddress = address(uint160(uint256(_withdrawalCredentials)));
}

function _processWCProof(ValidatorWitness calldata _witness) internal {
ValidatorStatus storage validatorStatus = validatorStatuses[_witness.validator.pubkey];
function _processWitnessProof(ValidatorWitness calldata _witness) internal {
ValidatorStatus storage validatorStatus = validatorStatuses[_witness.pubkey];

if (validatorStatus.bondStatus != BondStatus.AWAITING_PROOF) {
revert ValidatorNotPreDeposited();
}

(uint64 _wcVersion, address _wcAddress) = _deconstructWC(_witness.validator.withdrawalCredentials);
bytes32 _withdrawalCredentials = validatorStatus.stakingVault.withdrawalCredentials();

if (_wcVersion < 1) {
// ensures vault fair play
if (address(validatorStatus.stakingVault) != _wcToAddress(_withdrawalCredentials)) {
revert WithdrawalCredentialsAreInvalid();
}

_validateWCProof(_witness);
_validatePubKeyWCProof(_witness, _withdrawalCredentials);

// determine proof direction
if (address(validatorStatus.stakingVault) == _wcAddress) {
// stricter WC check to ensure WC version matches
if (validatorStatus.stakingVault.withdrawalCredentials() != _witness.validator.withdrawalCredentials) {
revert WithdrawalCredentialsAreInvalid();
}

validatorStatus.bondStatus = BondStatus.PROVED;
// TODO: positive events
} else {
validatorStatus.bondStatus = BondStatus.PROVED_INVALID;
nodeOperatorBonds[validatorStatus.nodeOperator].total -= PREDEPOSIT_AMOUNT;
// TODO: negative events
}
validatorStatus.bondStatus = BondStatus.PROVED;
nodeOperatorBonds[validatorStatus.nodeOperator].locked -= PREDEPOSIT_AMOUNT;

// TODO: positive events
}

// node operator accounting
Expand All @@ -254,6 +294,7 @@ contract PredepositGuarantee is CLProofVerifier {
error PredepositValueNotMultipleOfPrediposit();
error PredepositDepositAmountInvalid();
error MustBeNewValidatorPubkey();
error stakingVaultWithdrawalCredentialsMismatch();
error NotEnoughUnlockedCollateralToPredeposit();

// depositing errors
Expand Down

0 comments on commit 3bb45e8

Please sign in to comment.