Skip to content

Commit

Permalink
fix: use the validatorID instead of pubkey (ExocoreNetwork#124)
Browse files Browse the repository at this point in the history
* fix: use the validatorID instead of pubkey

On an Ethereum-based client chain, a validator container (generated as
part of the proof) does not have the raw public key stored within it.
Rather, it is the hash of the public key. This hash, therefore, cannot
be used to query the beacon chain API.

This PR proposes an alternative solution of using the `validatorIndex`,
that is, the position of the validator amongst all of the validators, be
used instead. Note that, according to the beacon spec, this number does
not change even as more validators enter or leave the network.

The `validatorIndex` is passed to the ExocoreGateway as a unique
identifier for it to send to the precompile, which refers to it as the
`validatorID`. For other chains, where a validator public key may be
directly available, the `validatorID` will be the public key instead of
the index. On Ethereum, the public key is 48 bytes (though we pass a
uint256 index which is 32 bytes). On Solana, the public key is 32 bytes
and on Sui it is 96 bytes. To handle the varying lengths, the public key
has been moved to the end of the payload, such that, it is the remainder
of the payload after parsing the staker and the amount.

BREAKING CHANGE
Since the PR modifies the format of the data sent over the wire, it is
not compatible with prior deployments of ClientChainGateway and
ExocoreGateway. If care is taken to ensure that there are no affected
messages currently in-flight and the implementations are updated in
tandem, it will be sufficient.

* chore: forge fmt

* fix(precompile): change param name to ValidatorID

* fix: rename pubkey to pubkeyHash

In all functions, accept the call to IETH_POS.stake, the pubkey is not
used directly. Instead it is the hashed public key.
  • Loading branch information
MaxMustermann2 authored Dec 3, 2024
1 parent 4a2c8ee commit e01d9e0
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 83 deletions.
78 changes: 39 additions & 39 deletions src/core/ExoCapsule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
event WithdrawalSuccess(address owner, address recipient, uint256 amount);

/// @notice Emitted when a partial withdrawal claim is successfully redeemed
/// @param pubkey The validator's BLS12-381 public key.
/// @param pubkeyHash The validator's BLS12-381 public key hash.
/// @param withdrawalEpoch The epoch at which the withdrawal was made.
/// @param recipient The address of the recipient of the withdrawal.
/// @param partialWithdrawalAmountGwei The amount of the partial withdrawal in Gwei.
event PartialWithdrawalRedeemed(
bytes32 pubkey, uint256 withdrawalEpoch, address indexed recipient, uint64 partialWithdrawalAmountGwei
bytes32 pubkeyHash, uint256 withdrawalEpoch, address indexed recipient, uint64 partialWithdrawalAmountGwei
);

/// @notice Emitted when an ETH validator is prove to have fully withdrawn from the beacon chain
/// @param pubkey The validator's BLS12-381 public key.
/// @param pubkeyHash The validator's BLS12-381 public key hash.
/// @param withdrawalEpoch The epoch at which the withdrawal was made.
/// @param recipient The address of the recipient of the withdrawal.
/// @param withdrawalAmountGwei The amount of the withdrawal in Gwei.
event FullWithdrawalRedeemed(
bytes32 pubkey, uint64 withdrawalEpoch, address indexed recipient, uint64 withdrawalAmountGwei
bytes32 pubkeyHash, uint64 withdrawalEpoch, address indexed recipient, uint64 withdrawalAmountGwei
);

/// @notice Emitted when capsuleOwner enables restaking
Expand All @@ -66,34 +66,34 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
event NonBeaconChainETHWithdrawn(address indexed recipient, uint256 amountWithdrawn);

/// @dev Thrown when the validator container is invalid.
/// @param pubkey The validator's BLS12-381 public key.
error InvalidValidatorContainer(bytes32 pubkey);
/// @param pubkeyHash The validator's BLS12-381 public key hash.
error InvalidValidatorContainer(bytes32 pubkeyHash);

/// @dev Thrown when the withdrawal container is invalid.
/// @param validatorIndex The validator index.
error InvalidWithdrawalContainer(uint64 validatorIndex);

/// @dev Thrown when a validator is double deposited.
/// @param pubkey The validator's BLS12-381 public key.
error DoubleDepositedValidator(bytes32 pubkey);
/// @param pubkeyHash The validator's BLS12-381 public key hash.
error DoubleDepositedValidator(bytes32 pubkeyHash);

/// @dev Thrown when a validator container is stale.
/// @param pubkey The validator's BLS12-381 public key.
/// @param pubkeyHash The validator's BLS12-381 public key hash.
/// @param timestamp The timestamp of the validator proof.
error StaleValidatorContainer(bytes32 pubkey, uint256 timestamp);
error StaleValidatorContainer(bytes32 pubkeyHash, uint256 timestamp);

/// @dev Thrown when a withdrawal has already been proven.
/// @param pubkey The validator's BLS12-381 public key.
/// @param pubkeyHash The validator's BLS12-381 public key hash.
/// @param withdrawalIndex The index of the withdrawal.
error WithdrawalAlreadyProven(bytes32 pubkey, uint256 withdrawalIndex);
error WithdrawalAlreadyProven(bytes32 pubkeyHash, uint256 withdrawalIndex);

/// @dev Thrown when a validator container is unregistered.
/// @param pubkey The validator's BLS12-381 public key.
error UnregisteredValidator(bytes32 pubkey);
/// @param pubkeyHash The validator's BLS12-381 public key hash.
error UnregisteredValidator(bytes32 pubkeyHash);

/// @dev Thrown when a validator container is unregistered or withdrawn.
/// @param pubkey The validator's BLS12-381 public key.
error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkey);
/// @param pubkeyHash The validator's BLS12-381 public key hash.
error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkeyHash);

/// @dev Thrown when the validator and withdrawal state roots do not match.
/// @param validatorStateRoot The state root of the validator container.
Expand All @@ -115,8 +115,8 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
error WithdrawalCredentialsNotMatch();

/// @dev Thrown when the validator container is inactive.
/// @param pubkey The validator's BLS12-381 public key.
error InactiveValidatorContainer(bytes32 pubkey);
/// @param pubkeyHash The validator's BLS12-381 public key hash.
error InactiveValidatorContainer(bytes32 pubkeyHash);

/// @dev Thrown when the caller of a message is not the gateway
/// @param gateway The address of the gateway.
Expand Down Expand Up @@ -162,20 +162,20 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
bytes32[] calldata validatorContainer,
BeaconChainProofs.ValidatorContainerProof calldata proof
) external onlyGateway returns (uint256 depositAmount) {
bytes32 validatorPubkey = validatorContainer.getPubkey();
bytes32 validatorPubkeyHash = validatorContainer.getPubkeyHash();
bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials();
Validator storage validator = _capsuleValidators[validatorPubkey];
Validator storage validator = _capsuleValidators[validatorPubkeyHash];

if (!validatorContainer.verifyValidatorContainerBasic()) {
revert InvalidValidatorContainer(validatorPubkey);
revert InvalidValidatorContainer(validatorPubkeyHash);
}

if (validator.status != VALIDATOR_STATUS.UNREGISTERED) {
revert DoubleDepositedValidator(validatorPubkey);
revert DoubleDepositedValidator(validatorPubkeyHash);
}

if (_isStaleProof(proof.beaconBlockTimestamp)) {
revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp);
revert StaleValidatorContainer(validatorPubkeyHash, proof.beaconBlockTimestamp);
}

if (withdrawalCredentials != bytes32(capsuleWithdrawalCredentials())) {
Expand All @@ -193,7 +193,7 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
depositAmount = depositAmountGwei * GWEI_TO_WEI;
}

_capsuleValidatorsByIndex[proof.validatorIndex] = validatorPubkey;
_capsuleValidatorsByIndex[proof.validatorIndex] = validatorPubkeyHash;
}

/// @inheritdoc IExoCapsule
Expand All @@ -203,24 +203,24 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
bytes32[] calldata withdrawalContainer,
BeaconChainProofs.WithdrawalProof calldata withdrawalProof
) external onlyGateway returns (bool partialWithdrawal, uint256 withdrawalAmount) {
bytes32 validatorPubkey = validatorContainer.getPubkey();
Validator storage validator = _capsuleValidators[validatorPubkey];
bytes32 validatorPubkeyHash = validatorContainer.getPubkeyHash();
Validator storage validator = _capsuleValidators[validatorPubkeyHash];
uint64 withdrawalEpoch = withdrawalProof.slotRoot.getWithdrawalEpoch();
partialWithdrawal = withdrawalEpoch < validatorContainer.getWithdrawableEpoch();
uint256 withdrawalId = uint256(withdrawalContainer.getWithdrawalIndex());

if (!validatorContainer.verifyValidatorContainerBasic()) {
revert InvalidValidatorContainer(validatorPubkey);
revert InvalidValidatorContainer(validatorPubkeyHash);
}
if (validator.status == VALIDATOR_STATUS.UNREGISTERED) {
revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey);
revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkeyHash);
}

if (provenWithdrawal[validatorPubkey][withdrawalId]) {
revert WithdrawalAlreadyProven(validatorPubkey, withdrawalId);
if (provenWithdrawal[validatorPubkeyHash][withdrawalId]) {
revert WithdrawalAlreadyProven(validatorPubkeyHash, withdrawalId);
}

provenWithdrawal[validatorPubkey][withdrawalId] = true;
provenWithdrawal[validatorPubkeyHash][withdrawalId] = true;

// Validate if validator and withdrawal proof state roots are the same
if (validatorProof.stateRoot != withdrawalProof.stateRoot) {
Expand All @@ -234,13 +234,13 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul

if (partialWithdrawal) {
// Immediately send ETH without sending request to Exocore side
emit PartialWithdrawalRedeemed(validatorPubkey, withdrawalEpoch, capsuleOwner, withdrawalAmountGwei);
emit PartialWithdrawalRedeemed(validatorPubkeyHash, withdrawalEpoch, capsuleOwner, withdrawalAmountGwei);
_sendETH(capsuleOwner, withdrawalAmountGwei * GWEI_TO_WEI);
} else {
// Full withdrawal
validator.status = VALIDATOR_STATUS.WITHDRAWN;
// If over MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32 * 1e9, then send remaining amount immediately
emit FullWithdrawalRedeemed(validatorPubkey, withdrawalEpoch, capsuleOwner, withdrawalAmountGwei);
emit FullWithdrawalRedeemed(validatorPubkeyHash, withdrawalEpoch, capsuleOwner, withdrawalAmountGwei);
if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) {
uint256 amountToSend = (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI;
_sendETH(capsuleOwner, amountToSend);
Expand Down Expand Up @@ -312,14 +312,14 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
return root;
}

/// @notice Gets the registered validator by pubkey.
/// @notice Gets the registered validator by pubkeyHash.
/// @dev The validator status must be registered. Reverts if not.
/// @param pubkey The validator's BLS12-381 public key.
/// @param pubkeyHash The validator's BLS12-381 public key hash.
/// @return The validator object, as defined in the `ExoCapsuleStorage`.
function getRegisteredValidatorByPubkey(bytes32 pubkey) public view returns (Validator memory) {
Validator memory validator = _capsuleValidators[pubkey];
function getRegisteredValidatorByPubkey(bytes32 pubkeyHash) public view returns (Validator memory) {
Validator memory validator = _capsuleValidators[pubkeyHash];
if (validator.status == VALIDATOR_STATUS.UNREGISTERED) {
revert UnregisteredValidator(pubkey);
revert UnregisteredValidator(pubkeyHash);
}

return validator;
Expand Down Expand Up @@ -366,7 +366,7 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul
proof.stateRootProof
);
if (!valid) {
revert InvalidValidatorContainer(validatorContainer.getPubkey());
revert InvalidValidatorContainer(validatorContainer.getPubkeyHash());
}
}

Expand Down
16 changes: 10 additions & 6 deletions src/core/ExocoreGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -374,21 +374,25 @@ contract ExocoreGateway is
onlyCalledFromThis
returns (bytes memory response)
{
bytes calldata validatorPubkey = payload[:32];
bytes calldata staker = payload[32:64];
uint256 amount = uint256(bytes32(payload[64:96]));
bytes calldata staker = payload[:32];
uint256 amount = uint256(bytes32(payload[32:64]));
// the length of the validatorID is not known. it depends on the chain.
// for Ethereum, it is the validatorIndex uint256 as bytes so it becomes 32. its value may be 0.
// for Solana, the pubkey is 32 bytes long but for Sui it is 96 bytes long.
// these chains do not have the concept of validatorIndex, so the raw key must be used.
bytes calldata validatorID = payload[64:];

bool isDeposit = act == Action.REQUEST_DEPOSIT_NST;
bool success;
if (isDeposit) {
(success,) = ASSETS_CONTRACT.depositNST(srcChainId, validatorPubkey, staker, amount);
(success,) = ASSETS_CONTRACT.depositNST(srcChainId, validatorID, staker, amount);
} else {
(success,) = ASSETS_CONTRACT.withdrawNST(srcChainId, validatorPubkey, staker, amount);
(success,) = ASSETS_CONTRACT.withdrawNST(srcChainId, validatorID, staker, amount);
}
if (isDeposit && !success) {
revert Errors.DepositRequestShouldNotFail(srcChainId, lzNonce); // we should not let this happen
}
emit NSTTransfer(isDeposit, success, bytes32(validatorPubkey), bytes32(staker), amount);
emit NSTTransfer(isDeposit, success, validatorID, bytes32(staker), amount);

response = isDeposit ? bytes("") : abi.encodePacked(lzNonce, success);
}
Expand Down
7 changes: 3 additions & 4 deletions src/core/NativeRestakingController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ abstract contract NativeRestakingController is
IExoCapsule capsule = _getCapsule(msg.sender);
uint256 depositValue = capsule.verifyDepositProof(validatorContainer, proof);

bytes32 validatorPubkey = validatorContainer.getPubkey();
bytes memory actionArgs = abi.encodePacked(validatorPubkey, bytes32(bytes20(msg.sender)), depositValue);
bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(msg.sender)), depositValue, proof.validatorIndex);

// deposit NST is a must-succeed action, so we don't need to check the response
_processRequest(Action.REQUEST_DEPOSIT_NST, actionArgs, bytes(""));
Expand All @@ -117,8 +116,8 @@ abstract contract NativeRestakingController is
capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof);
if (!partialWithdrawal) {
// request full withdraw
bytes32 validatorPubkey = validatorContainer.getPubkey();
bytes memory actionArgs = abi.encodePacked(validatorPubkey, bytes32(bytes20(msg.sender)), withdrawalAmount);
bytes memory actionArgs =
abi.encodePacked(bytes32(bytes20(msg.sender)), withdrawalAmount, validatorProof.validatorIndex);
bytes memory encodedRequest = abi.encode(VIRTUAL_NST_ADDRESS, msg.sender, withdrawalAmount);

// a full withdrawal needs response from Exocore, so we don't pass empty bytes
Expand Down
8 changes: 4 additions & 4 deletions src/interfaces/precompiles/IAssets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ interface IAssets {
/// @param clientChainID is the layerZero chainID if it is supported.
// It might be allocated by Exocore when the client chain isn't supported
// by layerZero
/// @param validatorPubkey The validator's pubkey
/// @param validatorID The validator's identifier: index (uint256 as bytes32) or pubkey.
/// @param stakerAddress The staker address
/// @param opAmount The amount to deposit
function depositNST(
uint32 clientChainID,
bytes calldata validatorPubkey,
bytes calldata validatorID,
bytes calldata stakerAddress,
uint256 opAmount
) external returns (bool success, uint256 latestAssetState);
Expand All @@ -67,12 +67,12 @@ interface IAssets {
/// @param clientChainID is the layerZero chainID if it is supported.
// It might be allocated by Exocore when the client chain isn't supported
// by layerZero
/// @param validatorPubkey The validator's pubkey
/// @param validatorID The validator's identifier: index (uint256 as bytes32) or pubkey.
/// @param withdrawAddress The withdraw address
/// @param opAmount The withdraw amount
function withdrawNST(
uint32 clientChainID,
bytes calldata validatorPubkey,
bytes calldata validatorID,
bytes calldata withdrawAddress,
uint256 opAmount
) external returns (bool success, uint256 latestAssetState);
Expand Down
14 changes: 10 additions & 4 deletions src/libraries/ActionAttributes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ library ActionAttributes {

uint256 internal constant MESSAGE_LENGTH_MASK = 0xFF; // 8 bits for message length
uint256 internal constant MESSAGE_LENGTH_SHIFT = 8;
uint256 internal constant MIN_LENGTH_FLAG = 1 << 16; // Flag at the 16th bit

function getAttributes(Action action) internal pure returns (uint256) {
uint256 attributes = 0;
Expand All @@ -29,13 +30,15 @@ library ActionAttributes {
attributes = LST | PRINCIPAL;
messageLength = ASSET_OPERATION_LENGTH;
} else if (action == Action.REQUEST_DEPOSIT_NST) {
attributes = NST | PRINCIPAL;
// we assume that a validatorID is at least 32 bytes, however, it is up for review.
attributes = NST | PRINCIPAL | MIN_LENGTH_FLAG;
messageLength = ASSET_OPERATION_LENGTH;
} else if (action == Action.REQUEST_WITHDRAW_LST) {
attributes = LST | PRINCIPAL | WITHDRAWAL;
messageLength = ASSET_OPERATION_LENGTH;
} else if (action == Action.REQUEST_WITHDRAW_NST) {
attributes = NST | PRINCIPAL | WITHDRAWAL;
// we assume that a validatorID is at least 32 bytes, however, it is up for review.
attributes = NST | PRINCIPAL | WITHDRAWAL | MIN_LENGTH_FLAG;
messageLength = ASSET_OPERATION_LENGTH;
} else if (action == Action.REQUEST_CLAIM_REWARD) {
attributes = REWARD | WITHDRAWAL;
Expand Down Expand Up @@ -80,8 +83,11 @@ library ActionAttributes {
return (getAttributes(action) & REWARD) != 0;
}

function getMessageLength(Action action) internal pure returns (uint256) {
return (getAttributes(action) >> MESSAGE_LENGTH_SHIFT) & MESSAGE_LENGTH_MASK;
function getMessageLength(Action action) internal pure returns (bool, uint256) {
uint256 attributes = getAttributes(action);
uint256 length = (attributes >> MESSAGE_LENGTH_SHIFT) & MESSAGE_LENGTH_MASK;
bool isMinLength = (attributes & MIN_LENGTH_FLAG) != 0;
return (isMinLength, length);
}

}
4 changes: 2 additions & 2 deletions src/libraries/ValidatorContainer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Endian} from "../libraries/Endian.sol";

/**
* class Validator(Container):
* pubkey: BLSPubkey
* pubkeyHash: The validator's BLS12-381 public key hash.
* withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals
* effective_balance: Gwei # Balance at stake
* slashed: boolean
Expand All @@ -26,7 +26,7 @@ library ValidatorContainer {
return validatorContainer.length == VALID_LENGTH;
}

function getPubkey(bytes32[] calldata validatorContainer) internal pure returns (bytes32) {
function getPubkeyHash(bytes32[] calldata validatorContainer) internal pure returns (bytes32) {
return validatorContainer[0];
}

Expand Down
8 changes: 4 additions & 4 deletions src/storage/ExoCapsuleStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ contract ExoCapsuleStorage {
/// @notice The address of the Beacon Chain Oracle contract.
IBeaconChainOracle public beaconOracle;

/// @dev Mapping of validator pubkey to their corresponding struct.
mapping(bytes32 pubkey => Validator validator) internal _capsuleValidators;
/// @dev Mapping of validator pubkey hash to their corresponding struct.
mapping(bytes32 pubkeyHash => Validator validator) internal _capsuleValidators;

/// @dev Mapping of validator index to their corresponding pubkey.
mapping(uint256 index => bytes32 pubkey) internal _capsuleValidatorsByIndex;
/// @dev Mapping of validator index to their corresponding pubkey hash.
mapping(uint256 index => bytes32 pubkeyHash) internal _capsuleValidatorsByIndex;

/// @notice This is a mapping of validatorPubkeyHash to withdrawal index to whether or not they have proven a
/// withdrawal
Expand Down
Loading

0 comments on commit e01d9e0

Please sign in to comment.