Skip to content

Commit

Permalink
feat: exit request data hash storage, triggerExitHashVerify method, h…
Browse files Browse the repository at this point in the history
…appyPath and triggerExitHashVerify tests
  • Loading branch information
Amuhar committed Jan 15, 2025
1 parent 084a62f commit 34651d2
Show file tree
Hide file tree
Showing 7 changed files with 473 additions and 64 deletions.
5 changes: 5 additions & 0 deletions contracts/0.8.9/oracle/IWithdrawalVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity 0.8.9;

interface IWithdrawalVault {
function addFullWithdrawalRequests(bytes[] calldata pubkeys) external;
}
134 changes: 134 additions & 0 deletions contracts/0.8.9/oracle/ValidatorsExitBus.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: 2023 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.9;

import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol";
import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol";
import { IWithdrawalVault } from "./IWithdrawalVault.sol";

contract ValidatorsExitBus is AccessControlEnumerable {
using UnstructuredStorage for bytes32;

/// @dev Errors
// error DuplicateExitRequest();
error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex);
error ZeroAddress();

/// Part of report data
struct ExitRequestData {
/// @dev Total number of validator exit requests in this report. Must not be greater
/// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport.
uint256 requestsCount;

/// @dev Format of the validator exit requests data. Currently, only the
/// DATA_FORMAT_LIST=1 is supported.
uint256 dataFormat;

/// @dev Validator exit requests data. Can differ based on the data format,
/// see the constant defining a specific data format below for more info.
bytes data;
}

// TODO: make type optimization
struct DeliveryHistory {
uint256 blockNumber;
/// @dev Key index in exit request array
uint256 lastDeliveredKeyIndex;
}
// TODO: make type optimization
struct RequestStatus {
// Total items count in report (by default type(uint32).max, update on first report unpack)
uint256 totalItemsCount;
// Total processed items in report (by default 0)
uint256 deliveredItemsCount;
// Vebo contract version at the time of hash submittion
uint256 contractVersion;
DeliveryHistory[] deliverHistory;
}

/// @notice The list format of the validator exit requests data. Used when all
/// requests fit into a single transaction.
///
/// Each validator exit request is described by the following 64-byte array:
///
/// MSB <------------------------------------------------------- LSB
/// | 3 bytes | 5 bytes | 8 bytes | 48 bytes |
/// | moduleId | nodeOpId | validatorIndex | validatorPubkey |
///
/// All requests are tightly packed into a byte array where requests follow
/// one another without any separator or padding, and passed to the `data`
/// field of the report structure.
///
/// Requests must be sorted in the ascending order by the following compound
/// key: (moduleId, nodeOpId, validatorIndex).
///
uint256 public constant DATA_FORMAT_LIST = 1;

/// Length in bytes of packed request
uint256 internal constant PACKED_REQUEST_LENGTH = 64;

/// Hash constant for mapping exit requests storage
bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION =
keccak256("lido.ValidatorsExitBus.reportHashes");

/// @dev Storage slot: address withdrawalVaultContract
bytes32 internal constant WITHDRAWAL_VAULT_CONTRACT_POSITION =
keccak256("lido.ValidatorsExitBus.withdrawalVaultContract");

// ILidoLocator internal immutable LOCATOR;

// TODO: read WV via locator
function _initialize_v2(address withdrawalVaultAddr) internal {
_setWithdrawalVault(withdrawalVaultAddr);
}

function _setWithdrawalVault(address addr) internal {
if (addr == address(0)) revert ZeroAddress();

WITHDRAWAL_VAULT_CONTRACT_POSITION.setStorageAddress(addr);
}

function triggerExitHashVerify(ExitRequestData calldata exitRequestData, uint256[] calldata keyIndexes) external payable {
bytes32 dataHash = keccak256(abi.encode(exitRequestData));
RequestStatus storage requestStatus = _storageExitRequestsHashes()[dataHash];

uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1;

uint256 offset;
bytes calldata data = exitRequestData.data;
bytes[] memory pubkeys = new bytes[](keyIndexes.length);

assembly {
offset := data.offset
}

for (uint256 i = 0; i < keyIndexes.length; i++) {
if (keyIndexes[i] > lastDeliveredKeyIndex) {
revert KeyWasNotUnpacked(keyIndexes[i], lastDeliveredKeyIndex);
}
uint256 requestOffset = offset + keyIndexes[i] * 64;

bytes calldata pubkey;

assembly {
pubkey.offset := add(requestOffset, 16)
pubkey.length := 48
}
pubkeys[i] = pubkey;

}

address withdrawalVaultAddr = WITHDRAWAL_VAULT_CONTRACT_POSITION.getStorageAddress();
IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests(pubkeys);
}

/// Storage helpers
function _storageExitRequestsHashes() internal pure returns (
mapping(bytes32 => RequestStatus) storage r
) {
bytes32 position = EXIT_REQUESTS_HASHES_POSITION;
assembly {
r.slot := position
}
}
}
78 changes: 34 additions & 44 deletions contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { PausableUntil } from "../utils/PausableUntil.sol";
import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol";

import { BaseOracle } from "./BaseOracle.sol";
import { ValidatorsExitBus } from "./ValidatorsExitBus.sol";


interface IOracleReportSanityChecker {
function checkExitBusOracleReport(uint256 _exitRequestsCount) external view;
}


contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus {
using UnstructuredStorage for bytes32;
using SafeCast for uint256;

Expand Down Expand Up @@ -109,6 +110,12 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
_initialize(consensusContract, consensusVersion, lastProcessingRefSlot);
}

// TODO: replace with locator
function finalizeUpgrade_v2(address withdrawalVaultAddress) external {
_updateContractVersion(2);
_initialize_v2(withdrawalVaultAddress);
}

/// @notice Resume accepting validator exit requests
///
/// @dev Reverts with `PausedExpected()` if contract is already resumed
Expand Down Expand Up @@ -161,40 +168,9 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
/// Requests data
///

/// @dev Total number of validator exit requests in this report. Must not be greater
/// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport.
uint256 requestsCount;

/// @dev Format of the validator exit requests data. Currently, only the
/// DATA_FORMAT_LIST=1 is supported.
uint256 dataFormat;

/// @dev Validator exit requests data. Can differ based on the data format,
/// see the constant defining a specific data format below for more info.
bytes data;
ExitRequestData exitRequestData;
}

/// @notice The list format of the validator exit requests data. Used when all
/// requests fit into a single transaction.
///
/// Each validator exit request is described by the following 64-byte array:
///
/// MSB <------------------------------------------------------- LSB
/// | 3 bytes | 5 bytes | 8 bytes | 48 bytes |
/// | moduleId | nodeOpId | validatorIndex | validatorPubkey |
///
/// All requests are tightly packed into a byte array where requests follow
/// one another without any separator or padding, and passed to the `data`
/// field of the report structure.
///
/// Requests must be sorted in the ascending order by the following compound
/// key: (moduleId, nodeOpId, validatorIndex).
///
uint256 public constant DATA_FORMAT_LIST = 1;

/// Length in bytes of packed request
uint256 internal constant PACKED_REQUEST_LENGTH = 64;

/// @notice Submits report data for processing.
///
/// @param data The data. See the `ReportData` structure's docs for details.
Expand All @@ -216,10 +192,12 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
{
_checkMsgSenderIsAllowedToSubmitData();
_checkContractVersion(contractVersion);
bytes32 exitRequestDataHash = keccak256(abi.encode(data.exitRequestData));
// it's a waste of gas to copy the whole calldata into mem but seems there's no way around
_checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data)));
_checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data.consensusVersion, data.refSlot, exitRequestDataHash)));
_startProcessing();
_handleConsensusReportData(data);
_storeOracleExitRequestHash(exitRequestDataHash, data, contractVersion);
}

/// @notice Returns the total number of validator exit requests ever processed
Expand Down Expand Up @@ -328,36 +306,37 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
}

function _handleConsensusReportData(ReportData calldata data) internal {
if (data.dataFormat != DATA_FORMAT_LIST) {
revert UnsupportedRequestsDataFormat(data.dataFormat);
if (data.exitRequestData.dataFormat != DATA_FORMAT_LIST) {
revert UnsupportedRequestsDataFormat(data.exitRequestData.dataFormat);
}

if (data.data.length % PACKED_REQUEST_LENGTH != 0) {
if (data.exitRequestData.data.length % PACKED_REQUEST_LENGTH != 0) {
revert InvalidRequestsDataLength();
}

// TODO: next iteration will check ref slot deliveredReportAmount
IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker())
.checkExitBusOracleReport(data.requestsCount);
.checkExitBusOracleReport(data.exitRequestData.requestsCount);

if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) {
if (data.exitRequestData.data.length / PACKED_REQUEST_LENGTH != data.exitRequestData.requestsCount) {
revert UnexpectedRequestsDataLength();
}

_processExitRequestsList(data.data);
_processExitRequestsList(data.exitRequestData.data);

_storageDataProcessingState().value = DataProcessingState({
refSlot: data.refSlot.toUint64(),
requestsCount: data.requestsCount.toUint64(),
requestsProcessed: data.requestsCount.toUint64(),
requestsCount: data.exitRequestData.requestsCount.toUint64(),
requestsProcessed: data.exitRequestData.requestsCount.toUint64(),
dataFormat: uint16(DATA_FORMAT_LIST)
});

if (data.requestsCount == 0) {
if (data.exitRequestData.requestsCount == 0) {
return;
}

TOTAL_REQUESTS_PROCESSED_POSITION.setStorageUint256(
TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.requestsCount
TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.exitRequestData.requestsCount
);
}

Expand Down Expand Up @@ -439,6 +418,17 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
return (moduleId << 40) | nodeOpId;
}

function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal {
mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes();
// if (hashes[hash].itemsCount > 0 ) revert DuplicateExitRequest();

RequestStatus storage request = hashes[exitRequestHash];
request.totalItemsCount = report.exitRequestData.requestsCount;
request.deliveredItemsCount = report.exitRequestData.requestsCount;
request.contractVersion = contractVersion;
request.deliverHistory.push(DeliveryHistory({blockNumber: block.number, lastDeliveredKeyIndex: report.exitRequestData.requestsCount - 1}));
}

///
/// Storage helpers
///
Expand Down
10 changes: 10 additions & 0 deletions test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pragma solidity 0.8.9;

contract WithdrawalVault__MockForVebo {

event AddFullWithdrawalRequestsCalled(bytes[] pubkeys);

function addFullWithdrawalRequests(bytes[] calldata pubkeys) external {
emit AddFullWithdrawalRequestsCalled(pubkeys);
}
}
Loading

0 comments on commit 34651d2

Please sign in to comment.