Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion l1-contracts/script/SetupAccess.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ contract SetupAccess is BaseScript {
);

bytes4[] memory publicSelectors = new bytes4[](0);
publicSelectors = new bytes4[](8);
publicSelectors = new bytes4[](12);
publicSelectors[0] = UniFiAVSManager.registerOperator.selector;
publicSelectors[1] = UniFiAVSManager.registerValidators.selector;
publicSelectors[2] = UniFiAVSManager.startDeregisterOperator.selector;
Expand All @@ -97,6 +97,10 @@ contract SetupAccess is BaseScript {
publicSelectors[5] = UniFiAVSManager.setOperatorCommitment.selector;
publicSelectors[6] = UniFiAVSManager.updateOperatorCommitment.selector;
publicSelectors[7] = UniFiAVSManager.registerOperatorWithCommitment.selector;
publicSelectors[8] = UniFiAVSManager.registerValidatorsOptimistically.selector;
publicSelectors[9] = UniFiAVSManager.verifyValidatorSignatures.selector;
publicSelectors[10] = UniFiAVSManager.slashValidatorsWithInvalidPubkey.selector;
publicSelectors[11] = UniFiAVSManager.slashValidatorsWithInvalidIndex.selector;

calldatas[1] = abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector,
Expand Down
292 changes: 281 additions & 11 deletions l1-contracts/src/UniFiAVSManager.sol

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions l1-contracts/src/interfaces/IUniFiAVSManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ pragma solidity >=0.8.0 <0.9.0;

import { IDelegationManager } from "eigenlayer/interfaces/IDelegationManager.sol";
import { ISignatureUtils } from "eigenlayer/interfaces/ISignatureUtils.sol";
import { BN254 } from "eigenlayer-middleware/libraries/BN254.sol";
import { IAVSDirectoryExtended } from "../interfaces/EigenLayer/IAVSDirectoryExtended.sol";
import "../structs/ValidatorData.sol";
import "../structs/OperatorData.sol";
import { BeaconChainHelperLib } from "../lib/BeaconChainHelperLib.sol";

/**
* @title IUniFiAVSManager
Expand Down Expand Up @@ -70,6 +72,24 @@ interface IUniFiAVSManager {
/// @notice Thrown when a restaking strategy allowlist update fails
error RestakingStrategyAllowlistUpdateFailed();

/// @notice Thrown when a salt is already used for a registration
error SaltAlreadyUsed();

/// @notice Thrown when a signature is expired
error SignatureExpired();

/// @notice Thrown when a validator proof is invalid
error InvalidValidatorProof(bytes32 blsPubKeyHash);

/// @notice Thrown when a validator index is already used
error ValidatorIndexAlreadyUsed();

/// @notice Thrown when an operator is slashed
error OperatorSlashed();

/// @notice Thrown when a validator is not backed by an EigenPod
error InvalidValidatorType();

/**
* @notice Emitted when a new operator is registered in the UniFi AVS.
* @param operator The address of the registered operator.
Expand Down Expand Up @@ -152,6 +172,20 @@ interface IUniFiAVSManager {
*/
event RestakingStrategyAllowlistUpdated(address indexed strategy, bool allowed);

/**
* @notice Emitted when a validator is slashed.
* @param operator The address of the operator managing the validator.
* @param blsPubKeyHash The BLS public key hash of the slashed validator.
*/
event ValidatorSlashed(address indexed operator, bytes32 indexed blsPubKeyHash);

/**
* @notice Emitted when the registration delay is set.
* @param oldDelay The previous registration delay value.
* @param newDelay The new registration delay value.
*/
event RegistrationDelaySet(uint64 oldDelay, uint64 newDelay);

/**
* @notice Returns the EigenPodManager contract.
* @return IEigenPodManager The EigenPodManager contract.
Expand Down Expand Up @@ -232,6 +266,13 @@ interface IUniFiAVSManager {
*/
function setDeregistrationDelay(uint64 newDelay) external;

/**
* @notice Sets a new registration delay for validators.
* @param newDelay The new registration delay in seconds.
* @dev Restricted to the DAO
*/
function setRegistrationDelay(uint64 newDelay) external;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO interface doesn't need to contain functions that are not meant to to be called by public. It adds unnecessary noise and LOC. I vote fore removing functions like this from the Interface and just keeping them + natspec in the contract

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is callable by DAO. So if our DAO becomes a contract in the future, we would need the interface for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but DAO contracts generally do not import interfaces. They deal with target & calldata. e.g. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/governance/Governor.sol


/**
* @notice Sets the chain ID for a specific index in the bitmap.
* @param index The index in the bitmap to set.
Expand All @@ -248,6 +289,29 @@ interface IUniFiAVSManager {
*/
function setAllowlistRestakingStrategy(address strategy, bool allowed) external;

/**
* @notice Registers validators optimistically.
* @param paramsArray The array of ValidatorRegistrationParams.
*/
function registerValidatorsOptimistically(ValidatorRegistrationParams[] calldata paramsArray) external;

/**
* @notice Verifies the signatures of validators.
* @param blsPubKeyHashes The BLS public key hashes of the validators.
*/
function verifyValidatorSignatures(bytes32[] calldata blsPubKeyHashes) external;

/**
* @notice Slashes validators with invalid pubkey.
* @param proofs The inclusion proofs for each validator.
*/
function slashValidatorsWithInvalidPubkey(BeaconChainHelperLib.InclusionProof[] calldata proofs) external;

/**
* @notice Slashes validators with invalid index.
* @param proofs The inclusion proofs for each validator.
*/
function slashValidatorsWithInvalidIndex(BeaconChainHelperLib.InclusionProof[] calldata proofs) external;
/**
* @notice Retrieves information about a specific operator.
* @param operator The address of the operator.
Expand Down Expand Up @@ -290,6 +354,12 @@ interface IUniFiAVSManager {
*/
function getDeregistrationDelay() external view returns (uint64);

/**
* @notice Retrieves the current registration delay for validators.
* @return The current registration delay in seconds.
*/
function getRegistrationDelay() external view returns (uint64);

/**
* @notice Converts a bitmap to an array of chain IDs.
* @param bitmap The bitmap to convert.
Expand Down Expand Up @@ -330,4 +400,18 @@ interface IUniFiAVSManager {

/// @notice Returns the EigenLayer AVSDirectory contract.
function avsDirectory() external view returns (address);

/**
* @notice Returns the BLS message hash for a validator registration.
* @param typeHash The type hash for the message.
* @param operator The address of the operator.
* @param salt The salt for the message.
* @param expiry The expiry for the message.
* @param index The index for the message.
* @return BN254.G1Point The BLS message hash.
*/
function blsMessageHash(bytes32 typeHash, address operator, bytes32 salt, uint256 expiry, uint256 index)
external
view
returns (BN254.G1Point memory);
}
92 changes: 76 additions & 16 deletions l1-contracts/src/lib/BeaconChainHelperLib.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,83 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { MerkleUtils } from "./MerkleUtils.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in general, MerkleProof you have less control over the structure of provided proof.

1- MerkleUtils has a merkleize() function to construct a Merkle tree from an array of chunks. which is used for constructing the validator tree hash. It's important that we pass the validator information so we can check them in the sc and contruct the validator tree hash
2- proof verification takes an additional paramter index which helps with partial proofs.
3- not a big reason but we would need to pass a hashing function that uses sha256 for MerkleProof since the default one is keccak256

I am experimenting with the proving logic in general and trying to find a good balance between readability and control of the proofs.


library BeaconChainHelperLib {
address internal constant _BEACON_ROOT_CONTRACT = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;

struct InclusionProof {
// `Chunks` of the SSZ encoded validator
bytes32[8] validator;
// Index of the validator in the beacon state validator list
uint256 validatorIndex;
// Proof of inclusion of validator in beacon state validator list
bytes32[] validatorProof;
// Root of the validator list in the beacon state
bytes32 validatorsRoot;
// Proof of inclusion of validator list in the beacon state
bytes32[] beaconStateProof;
// Root of the beacon state
bytes32 beaconStateRoot;
// Proof of inclusion of beacon state in the beacon block
bytes32[] beaconBlockProofForState;
// Proof of inclusion of the validator index in the beacon block. leave this empty if not needed.
bytes32[] beaconBlockProofForProposerIndex;
// Timestamp of the beacon block
uint256 timestamp;
}

/// @dev The validator pub key failed verification against the pub key hash tree root in the validator chunks
error InvalidValidatorBLSPubKey();
/// @dev The proof that the validator is a part of the validator list is invalid.
error ValidatorProofFailed();
/// @dev The proof that the validator list is a part of the beacon state is invalid.
error BeaconStateProofFailed();
/// @dev The proof that the beacon state is a part of the beacon block is invalid.
error BeaconBlockProofForStateFailed();
/// @dev The proof that the actual validator index is a part of the beacon is invalid.
error BeaconBlockProofForProposerIndex();

function verifyValidatorExistence(InclusionProof memory inclusionProof) internal returns (bool) {
(, bytes32 beaconBlockRoot) = getRootFromTimestamp(inclusionProof.timestamp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved down in this function after the 2 proof checks to save on some gas in case of bad proof

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call! although this would revert the outer call so the whole transaction should be reverting. But I think it's good to add it in case we use this in another place.


// Validator is verified against the validator list in the beacon state
bytes32 validatorHashTreeRoot = MerkleUtils.merkleize(inclusionProof.validator);
if (
!MerkleUtils.verifyProof(
inclusionProof.validatorProof,
inclusionProof.validatorsRoot,
validatorHashTreeRoot,
inclusionProof.validatorIndex
)
) {
// Revert if the proof that the expected validator is a part of the validator
// list in beacon state fails
return false;
}

if (
!MerkleUtils.verifyProof(
inclusionProof.beaconStateProof, inclusionProof.beaconStateRoot, inclusionProof.validatorsRoot, 11
)
) {
// Revert if the proof that the validator list is a part of the beacon state fails
return false;
}

// Beacon state is verified against the beacon block
if (
!MerkleUtils.verifyProof(
inclusionProof.beaconBlockProofForState, beaconBlockRoot, inclusionProof.beaconStateRoot, 3
)
) {
// Revert if the proof for the beacon state being a part of the beacon block fails
return false;
}

return true;
}

function verifyProposerAt(uint256 timestamp, uint256 proposerIndex, bytes32[2] memory proof)
internal
returns (bool)
Expand All @@ -16,8 +90,8 @@ library BeaconChainHelperLib {

bytes32 slotAndProposerIndexNode = sha256(
abi.encodePacked(
abi.encodePacked(to_little_endian_64(uint64(slot)), bytes24(0)),
abi.encodePacked(to_little_endian_64(uint64(proposerIndex)), bytes24(0))
abi.encodePacked(MerkleUtils.to_little_endian_64(uint64(slot)), bytes24(0)),
abi.encodePacked(MerkleUtils.to_little_endian_64(uint64(proposerIndex)), bytes24(0))
)
);

Expand All @@ -28,20 +102,6 @@ library BeaconChainHelperLib {
return root == beaconRootFromChain;
}

function to_little_endian_64(uint64 value) internal pure returns (bytes memory ret) {
ret = new bytes(8);
bytes8 bytesValue = bytes8(value);
// Byteswapping during copying to bytes.
ret[0] = bytesValue[7];
ret[1] = bytesValue[6];
ret[2] = bytesValue[5];
ret[3] = bytesValue[4];
ret[4] = bytesValue[3];
ret[5] = bytesValue[2];
ret[6] = bytesValue[1];
ret[7] = bytesValue[0];
}

function getRootFromTimestamp(uint256 timestamp) internal returns (bool, bytes32) {
(bool ret, bytes memory data) = _BEACON_ROOT_CONTRACT.call(bytes.concat(bytes32(timestamp)));
return (ret, bytes32(data));
Expand Down
86 changes: 86 additions & 0 deletions l1-contracts/src/lib/MerkleUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0 <0.9.0;

library MerkleUtils {
uint256 internal constant CHUNKS_LENGTH = 8;
uint256 internal constant TMP_LENGTH = 4;

function hash(bytes32 a, bytes32 b) internal pure returns (bytes32) {
return sha256(abi.encodePacked(a, b));
}

function merkleize(bytes32[CHUNKS_LENGTH] memory chunks) internal pure returns (bytes32) {
bytes32[] memory tmp = new bytes32[](TMP_LENGTH);

for (uint256 i; i < CHUNKS_LENGTH; ++i) {
merge(tmp, i, chunks[i]);
}

return tmp[TMP_LENGTH - 1];
}

function merge(bytes32[] memory tmp, uint256 index, bytes32 chunk) internal pure {
bytes32 h = chunk;
uint256 j = 0;
while (true) {
if (index & 1 << j == 0) {
break;
} else {
h = hash(tmp[j], h);
}
j += 1;
}
tmp[j] = h;
}

function verifyProof(bytes32[] memory proof, bytes32 root, bytes32 leaf, uint256 leafIndex)
internal
pure
returns (bool)
{
bytes32 h = leaf;
uint256 index = leafIndex;

for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];

if (index % 2 == 0) {
h = sha256(bytes.concat(h, proofElement));
} else {
h = sha256(bytes.concat(proofElement, h));
}

index = index / 2;
}

return h == root;
}

function toLittleEndian(uint256 n) internal pure returns (bytes32) {
uint256 v = n;
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 to_little_endian_64(uint64 value) internal pure returns (bytes memory ret) {
ret = new bytes(8);
bytes8 bytesValue = bytes8(value);
// Byteswapping during copying to bytes.
ret[0] = bytesValue[7];
ret[1] = bytesValue[6];
ret[2] = bytesValue[5];
ret[3] = bytesValue[4];
ret[4] = bytesValue[3];
ret[5] = bytesValue[2];
ret[6] = bytesValue[1];
ret[7] = bytesValue[0];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
pragma solidity >=0.8.0 <0.9.0;

import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./structs/ValidatorData.sol";
import "./structs/OperatorData.sol";
import "../structs/ValidatorData.sol";
import "../structs/OperatorData.sol";

/**
* @title UniFiAVSManagerStorage
Expand All @@ -22,6 +22,12 @@ abstract contract UniFiAVSManagerStorage {
mapping(uint256 => uint8) chainIdToBitmapIndex;
// Set of allowlisted restaking strategies
EnumerableSet.AddressSet allowlistedRestakingStrategies;
// Mapping to store validator registration data
mapping(bytes32 => ValidatorRegistrationData) validatorRegistrations;
// Delay after which a validator can be considered registered
uint64 registerationDelay;
// Slashed operators mapping
mapping(address operator => InvalidValidator[] invalidValidators) slashedOperators;
}

/**
Expand Down
Loading