diff --git a/zilliqa/src/contracts/deposit_v4.sol b/zilliqa/src/contracts/deposit_v4.sol index 35c3b5df5..9f5db1f2b 100644 --- a/zilliqa/src/contracts/deposit_v4.sol +++ b/zilliqa/src/contracts/deposit_v4.sol @@ -38,6 +38,7 @@ struct Committee { uint256 totalStake; bytes[] stakerKeys; mapping(bytes => CommitteeStakerEntry) stakers; + uint64 epoch; } struct Staker { @@ -81,15 +82,10 @@ contract Deposit is UUPSUpgradeable { /// @custom:storage-location erc7201:zilliqa.storage.DepositStorage struct DepositStorage { - // The committee in the current epoch and the 2 epochs following it. The value for the current epoch - // is stored at index (currentEpoch() % 3). + // The last 3 committees which had changes. The current committee is the one with the largest epoch. Committee[3] _committee; // All stakers. Keys into this map are stored by the `Committee`. mapping(bytes => Staker) _stakersMap; - // The latest epoch for which the committee was calculated. It is implied that no changes have (yet) occurred in - // future epochs, either because those epochs haven't happened yet or because they have happened, but no deposits - // or withdrawals were made. - uint64 latestComputedEpoch; uint256 minimumStake; uint256 maximumStakers; uint64 blocksPerEpoch; @@ -150,16 +146,19 @@ contract Deposit is UUPSUpgradeable { function committee() private view returns (Committee storage) { DepositStorage storage $ = _getDepositStorage(); - if ($.latestComputedEpoch <= currentEpoch()) { - // If the current epoch is after the latest computed epoch, it is implied that no changes have happened to - // the committee since the latest computed epoch. Therefore, it suffices to return the committee at that - // latest computed epoch. - return $._committee[$.latestComputedEpoch % 3]; - } else { - // Otherwise, the committee has been changed. The caller who made the change will have pre-computed the - // result for us, so we can just return it. - return $._committee[currentEpoch() % 3]; + // The current committee is the one whose epoch is the largest out of those less than or equal to currentEpoch(). + // Any committees with an epoch largest than current is a future committee. + Committee storage currentCommittee = $._committee[0]; + for ( + uint256 i = 1; + i < $._committee.length; + i++ + ) { + if ($._committee[i].epoch <= currentEpoch() && $._committee[i].epoch > currentCommittee.epoch) { + currentCommittee = $._committee[i]; + } } + return currentCommittee; } function minimumStake() public view returns (uint256) { @@ -217,13 +216,104 @@ contract Deposit is UUPSUpgradeable { return committee().totalStake; } - function getFutureTotalStake() public view returns (uint256) { + function getFutureCommittee() private view returns (Committee storage) { DepositStorage storage $ = _getDepositStorage(); - // if `latestComputedEpoch > currentEpoch()` - // then `latestComputedEpoch` determines the future committee we need - // otherwise there are no committee changes after `currentEpoch()` - // i.e. `latestComputedEpoch` determines the most recent committee - return $._committee[$.latestComputedEpoch % 3].totalStake; + // The future committee is determined by the committee with the largest epoch + Committee storage futureCommittee = $._committee[0]; + for ( + uint256 i = 1; + i < $._committee.length; + i++ + ) { + if ($._committee[i].epoch > futureCommittee.epoch) { + futureCommittee = $._committee[i]; + } + } + return futureCommittee; + } + + function getOrCreateFutureCommittee() private returns (Committee storage) { + DepositStorage storage $ = _getDepositStorage(); + + Committee storage futureCommittee = getFutureCommittee(); + // Future Committee may already exist, return immediately + if (futureCommittee.epoch == currentEpoch() + 2) { + return futureCommittee; + } + + // Create a new committee. Overwrite the committee with the smallest epoch which by now will have passed + Committee storage committeeToOverwrite = $._committee[0]; + for ( + uint256 i = 1; + i < $._committee.length; + i++ + ) { + if ($._committee[i].epoch < committeeToOverwrite.epoch) { + committeeToOverwrite = $._committee[i]; + } + } + + committeeToOverwrite.epoch = currentEpoch() + 2; + + // Now set new committee to be the same as that of the most recent update + // The operation we want to do is: committeeToOverwrite = futureCommittee but we are careful to write only when necessary + + // Overwrite existing staker's data if necessary + bool stakerIndexChanged = false; + for ( + uint256 j = 0; + j < committeeToOverwrite.stakerKeys.length; + j++ + ) { + bytes memory stakerKey = committeeToOverwrite.stakerKeys[j]; + CommitteeStakerEntry storage stakerInFutureCommittee = futureCommittee.stakers[stakerKey]; + // If staker exists in future committee then update in new + if (stakerInFutureCommittee.index != 0) { + CommitteeStakerEntry storage stakerInCommitteeToOverwrite = committeeToOverwrite.stakers[stakerKey]; + if (stakerInFutureCommittee.index != stakerInCommitteeToOverwrite.index) { + stakerIndexChanged = true; + stakerInCommitteeToOverwrite.index = stakerInFutureCommittee.index; + } + if (stakerInFutureCommittee.balance != stakerInCommitteeToOverwrite.balance) { + stakerInCommitteeToOverwrite.balance = stakerInFutureCommittee.balance; + } + // Otherwise remove them + } else { + delete committeeToOverwrite.stakers[stakerKey]; + stakerIndexChanged = true; + } + } + + // Now add any stakers which are in future committee which but not in our new committee + for ( + uint256 j = 0; + j < futureCommittee.stakerKeys.length; + j++ + ) { + bytes storage stakerKey = futureCommittee + .stakerKeys[j]; + if (committeeToOverwrite.stakers[stakerKey].index == 0) { + committeeToOverwrite.stakers[ + stakerKey + ] = futureCommittee.stakers[stakerKey]; + stakerIndexChanged = true; + } + } + + if (futureCommittee.totalStake != committeeToOverwrite.totalStake) { + committeeToOverwrite.totalStake = futureCommittee + .totalStake; + } + if (stakerIndexChanged) { + committeeToOverwrite.stakerKeys = futureCommittee + .stakerKeys; + } + + return committeeToOverwrite; + } + + function getFutureTotalStake() public view returns (uint256) { + return getFutureCommittee().totalStake; } function getStakersData() @@ -286,19 +376,10 @@ contract Deposit is UUPSUpgradeable { if (blsPubKey.length != 48) { revert UnexpectedArgumentLength("bls public key", 48); } - DepositStorage storage $ = _getDepositStorage(); - - // if `latestComputedEpoch > currentEpoch()` - // then `latestComputedEpoch` determines the future committee we need - // otherwise there are no committee changes after `currentEpoch()` - // i.e. `latestComputedEpoch` determines the most recent committee - Committee storage latestCommittee = $._committee[ - $.latestComputedEpoch % 3 - ]; // We don't need to check if `blsPubKey` is in `stakerKeys` here. If the `blsPubKey` is not a staker, the // balance will default to zero. - return latestCommittee.stakers[blsPubKey].balance; + return getFutureCommittee().stakers[blsPubKey].balance; } function getRewardAddress( @@ -383,88 +464,14 @@ contract Deposit is UUPSUpgradeable { return $._stakersMap[blsPubKey].peerId; } - function updateLatestComputedEpoch() internal { - DepositStorage storage $ = _getDepositStorage(); - // If the latest computed epoch is less than two epochs ahead of the current one, we must fill in the missing - // epochs. This just involves copying the committee from the previous epoch to the next one. It is assumed that - // the caller will then want to update the future epochs. - if ($.latestComputedEpoch < currentEpoch() + 2) { - Committee storage latestComputedCommittee = $._committee[ - $.latestComputedEpoch % 3 - ]; - // Note the early exit condition if `latestComputedEpoch + 3` which ensures this loop will not run more - // than twice. This is acceptable because we only store 3 committees at a time, so once we have updated two - // of them to the latest computed committee, there is no more work to do. - for ( - uint64 i = $.latestComputedEpoch + 1; - i <= currentEpoch() + 2 && i < $.latestComputedEpoch + 3; - i++ - ) { - // The operation we want to do is: `_committee[i % 3] = latestComputedCommittee` but we are careful to write only when necessary - Committee storage committeeToUpdate = $._committee[i % 3]; - bool stakerIndexChanged = false; - - // Overwrite existing staker's data - for ( - uint256 j = 0; - j < committeeToUpdate.stakerKeys.length; - j++ - ) { - bytes memory stakerKey = committeeToUpdate.stakerKeys[j]; - CommitteeStakerEntry storage stakerInLatestCommittee = latestComputedCommittee.stakers[stakerKey]; - // If staker exists in latest then update in new - if (stakerInLatestCommittee.index != 0) { - CommitteeStakerEntry storage stakerInCommitteeToUpdate = committeeToUpdate.stakers[stakerKey]; - if (stakerInLatestCommittee.index != stakerInCommitteeToUpdate.index) { - stakerIndexChanged = true; - stakerInCommitteeToUpdate.index = stakerInLatestCommittee.index; - } - if (stakerInLatestCommittee.balance != stakerInCommitteeToUpdate.balance) { - stakerInCommitteeToUpdate.balance = stakerInLatestCommittee.balance; - } - // Otherwise remove them - } else { - delete committeeToUpdate.stakers[stakerKey]; - stakerIndexChanged = true; - } - } - - // Now add any new stakers - for ( - uint256 j = 0; - j < latestComputedCommittee.stakerKeys.length; - j++ - ) { - bytes storage stakerKey = latestComputedCommittee - .stakerKeys[j]; - if (committeeToUpdate.stakers[stakerKey].index == 0) { - committeeToUpdate.stakers[ - stakerKey - ] = latestComputedCommittee.stakers[stakerKey]; - stakerIndexChanged = true; - } - } - - if (latestComputedCommittee.totalStake != committeeToUpdate.totalStake) { - committeeToUpdate.totalStake = latestComputedCommittee - .totalStake; - } - if (stakerIndexChanged) { - committeeToUpdate.stakerKeys = latestComputedCommittee - .stakerKeys; - } - } - - $.latestComputedEpoch = currentEpoch() + 2; - } - } - // Returns the next block number at which new stakers are added, // existing ones removed and/or deposits of existing stakers change function nextUpdate() public view returns (uint256 blockNumber) { - DepositStorage storage $ = _getDepositStorage(); - if ($.latestComputedEpoch > currentEpoch()) - blockNumber = $.latestComputedEpoch * $.blocksPerEpoch; + uint256 latestCommitteeEpoch = getFutureCommittee().epoch; + if (latestCommitteeEpoch > currentEpoch()) { + DepositStorage storage $ = _getDepositStorage(); + blockNumber = latestCommitteeEpoch * $.blocksPerEpoch; + } } // keep in-sync with zilliqa/src/precompiles.rs @@ -536,11 +543,7 @@ contract Deposit is UUPSUpgradeable { staker.signingAddress = signingAddress; staker.controlAddress = msg.sender; - updateLatestComputedEpoch(); - - Committee storage futureCommittee = $._committee[ - (currentEpoch() + 2) % 3 - ]; + Committee storage futureCommittee = getOrCreateFutureCommittee(); if (futureCommittee.stakerKeys.length >= $.maximumStakers) { revert TooManyStakers(); @@ -557,22 +560,10 @@ contract Deposit is UUPSUpgradeable { futureCommittee.stakerKeys.push(blsPubKey); emit StakerAdded(blsPubKey, nextUpdate(), msg.value); - } function depositTopup(bytes calldata blsPubKey) public payable onlyControlAddress(blsPubKey) { - DepositStorage storage $ = _getDepositStorage(); - - bytes storage stakerKey = $._stakerKeys[msg.sender]; - if (stakerKey.length == 0) { - revert KeyNotStaked(); - } - - updateLatestComputedEpoch(); - - Committee storage futureCommittee = $._committee[ - (currentEpoch() + 2) % 3 - ]; + Committee storage futureCommittee = getFutureCommittee(); if (futureCommittee.stakers[blsPubKey].index == 0) { revert KeyNotStaked(); } @@ -590,11 +581,8 @@ contract Deposit is UUPSUpgradeable { function unstake(bytes calldata blsPubKey, uint256 amount) public onlyControlAddress(blsPubKey) { DepositStorage storage $ = _getDepositStorage(); - updateLatestComputedEpoch(); - Committee storage futureCommittee = $._committee[ - (currentEpoch() + 2) % 3 - ]; + Committee storage futureCommittee = getFutureCommittee(); if (futureCommittee.stakers[blsPubKey].index == 0) { revert KeyNotStaked(); } @@ -712,4 +700,4 @@ contract Deposit is UUPSUpgradeable { (bool sent, ) = msg.sender.call{value: releasedAmount}(""); require(sent, "failed to send"); } -} +} \ No newline at end of file diff --git a/zilliqa/src/contracts/deposit_v5.sol b/zilliqa/src/contracts/deposit_v5.sol deleted file mode 100644 index 51f54e482..000000000 --- a/zilliqa/src/contracts/deposit_v5.sol +++ /dev/null @@ -1,705 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.20; - -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {Deque, Withdrawal} from "./utils/deque.sol"; - -using Deque for Deque.Withdrawals; - -/// Argument has unexpected length -/// @param argument name of argument -/// @param required expected length -error UnexpectedArgumentLength(string argument, uint256 required); - -/// Message sender does not control the key it is attempting to modify -error Unauthorised(); -/// Maximum number of stakers has been reached -error TooManyStakers(); -/// Key already staked -error KeyAlreadyStaked(); -/// Key is not staked -error KeyNotStaked(); -/// Stake amount less than minimum -error StakeAmountTooLow(); - -/// Proof of possession verification failed -error RogueKeyCheckFailed(); - -struct CommitteeStakerEntry { - // The index of the value in the `stakers` array plus 1. - // Index 0 is used to mean a value is not present. - uint256 index; - // Invariant: `balance >= minimumStake` - uint256 balance; -} - -struct Committee { - // Invariant: Equal to the sum of `balances` in `stakers`. - uint256 totalStake; - bytes[] stakerKeys; - mapping(bytes => CommitteeStakerEntry) stakers; - uint64 epoch; -} - -struct Staker { - // The address used for authenticating requests from this staker to the deposit contract. - // Invariant: `controlAddress != address(0)`. - address controlAddress; - // The address which rewards for this staker will be sent to. - address rewardAddress; - // libp2p peer ID, corresponding to the staker's `blsPubKey` - bytes peerId; - // Invariants: Items are always sorted by `startedAt`. No two items have the same value of `startedAt`. - Deque.Withdrawals withdrawals; - // The address whose key with which validators sign cross-chain events - address signingAddress; -} - -contract Deposit is UUPSUpgradeable { - // Emitted to inform that a new staker identified by `blsPubKey` - // is going to be added to the committee `atFutureBlock`, increasing - // the total stake by `newStake` - event StakerAdded(bytes blsPubKey, uint256 atFutureBlock, uint256 newStake); - - // Emitted to inform that the staker identified by `blsPubKey` - // is going to be removed from the committee `atFutureBlock` - event StakerRemoved(bytes blsPubKey, uint256 atFutureBlock); - - // Emitted to inform that the deposited stake of the staker - // identified by `blsPubKey` is going to change to `newStake` - // at `atFutureBlock` - event StakeChanged( - bytes blsPubKey, - uint256 atFutureBlock, - uint256 newStake - ); - - // Emitted to inform that the staker identified by `blsPubKey` - // has updated its data that can be refetched using `getStakerData()` - event StakerUpdated(bytes blsPubKey); - - uint64 public constant VERSION = 3; - - /// @custom:storage-location erc7201:zilliqa.storage.DepositStorage - struct DepositStorage { - // The last 3 committees which had changes. The current committee is the one with the largest epoch. - Committee[3] _committee; - // All stakers. Keys into this map are stored by the `Committee`. - mapping(bytes => Staker) _stakersMap; - // The latest epoch for which the committee was calculated. It is implied that no changes have (yet) occurred in - // future epochs, either because those epochs haven't happened yet or because they have happened, but no deposits - // or withdrawals were made. - uint64 latestComputedEpoch; - uint256 minimumStake; - uint256 maximumStakers; - uint64 blocksPerEpoch; - } - - modifier onlyControlAddress(bytes calldata blsPubKey) { - DepositStorage storage $ = _getDepositStorage(); - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - if ($._stakersMap[blsPubKey].controlAddress != msg.sender) { - revert Unauthorised(); - } - _; - } - - // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.DepositStorage")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant DEPOSIT_STORAGE_LOCATION = - 0x958a6cf6390bd7165e3519675caa670ab90f0161508a9ee714d3db7edc507400; - - function _getDepositStorage() - private - pure - returns (DepositStorage storage $) - { - assembly { - $.slot := DEPOSIT_STORAGE_LOCATION - } - } - - function version() public view returns (uint64) { - return _getInitializedVersion(); - } - - function _authorizeUpgrade( - // solhint-disable-next-line no-unused-vars - address newImplementation - ) internal virtual override { - require( - msg.sender == address(0), - "system contract must be upgraded by the system" - ); - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - // explicitly set version number in contract code - // solhint-disable-next-line no-empty-blocks - function reinitialize() public reinitializer(VERSION) {} - - function currentEpoch() public view returns (uint64) { - DepositStorage storage $ = _getDepositStorage(); - return uint64(block.number / $.blocksPerEpoch); - } - - function committee() private view returns (Committee storage) { - DepositStorage storage $ = _getDepositStorage(); - // The current committee is the one whose epoch is the largest out of those less than or equal to currentEpoch(). - // Any committees with an epoch largest than current is a future committee. - Committee storage currentCommittee = $._committee[0]; - for ( - uint256 i = 1; - i < $._committee.length; - i++ - ) { - if ($._committee[i].epoch <= currentEpoch() && $._committee[i].epoch > currentCommittee.epoch) { - currentCommittee = $._committee[i]; - } - } - return currentCommittee; - } - - function minimumStake() public view returns (uint256) { - DepositStorage storage $ = _getDepositStorage(); - return $.minimumStake; - } - - function maximumStakers() public view returns (uint256) { - DepositStorage storage $ = _getDepositStorage(); - return $.maximumStakers; - } - - function blocksPerEpoch() public view returns (uint64) { - DepositStorage storage $ = _getDepositStorage(); - return $.blocksPerEpoch; - } - - function leaderFromRandomness( - uint256 randomness - ) private view returns (bytes memory) { - Committee storage currentCommittee = committee(); - // Get a random number in the inclusive range of 0 to (totalStake - 1) - uint256 position = randomness % currentCommittee.totalStake; - uint256 cummulativeStake = 0; - - // TODO: Consider binary search for performance. Or consider an alias method for O(1) performance. - for (uint256 i = 0; i < currentCommittee.stakerKeys.length; i++) { - bytes memory stakerKey = currentCommittee.stakerKeys[i]; - uint256 stakedBalance = currentCommittee.stakers[stakerKey].balance; - - cummulativeStake += stakedBalance; - - if (position < cummulativeStake) { - return stakerKey; - } - } - - revert("Unable to select next leader"); - } - - function leaderAtView( - uint256 viewNumber - ) public view returns (bytes memory) { - uint256 randomness = uint256( - keccak256(bytes.concat(bytes32(viewNumber))) - ); - return leaderFromRandomness(randomness); - } - - function getStakers() public view returns (bytes[] memory) { - return committee().stakerKeys; - } - - function getTotalStake() public view returns (uint256) { - return committee().totalStake; - } - - function getFutureCommittee() private view returns (Committee storage) { - DepositStorage storage $ = _getDepositStorage(); - // The future committee is determined by the committee with the largest epoch - Committee storage futureCommittee = $._committee[0]; - for ( - uint256 i = 1; - i < $._committee.length; - i++ - ) { - if ($._committee[i].epoch > futureCommittee.epoch) { - futureCommittee = $._committee[i]; - } - } - return futureCommittee; - } - - function getOrCreateFutureCommittee() private returns (Committee storage) { - DepositStorage storage $ = _getDepositStorage(); - - Committee storage futureCommittee = getFutureCommittee(); - // Future Committee may already exist, return immediately - if (futureCommittee.epoch == currentEpoch() + 2) { - return futureCommittee; - } - - // Create a new committee. Overwrite the committee with the smallest epoch which by now will have passed - Committee storage committeeToOverwrite = $._committee[0]; - for ( - uint256 i = 1; - i < $._committee.length; - i++ - ) { - if ($._committee[i].epoch < committeeToOverwrite.epoch) { - committeeToOverwrite = $._committee[i]; - } - } - - committeeToOverwrite.epoch = currentEpoch() + 2; - - // Now set new committee to be the same as that of the most recent update - // The operation we want to do is: committeeToOverwrite = futureCommittee but we are careful to write only when necessary - - // Overwrite existing staker's data if necessary - bool stakerIndexChanged = false; - for ( - uint256 j = 0; - j < committeeToOverwrite.stakerKeys.length; - j++ - ) { - bytes memory stakerKey = committeeToOverwrite.stakerKeys[j]; - CommitteeStakerEntry storage stakerInFutureCommittee = futureCommittee.stakers[stakerKey]; - // If staker exists in future committee then update in new - if (stakerInFutureCommittee.index != 0) { - CommitteeStakerEntry storage stakerInCommitteeToOverwrite = committeeToOverwrite.stakers[stakerKey]; - if (stakerInFutureCommittee.index != stakerInCommitteeToOverwrite.index) { - stakerIndexChanged = true; - stakerInCommitteeToOverwrite.index = stakerInFutureCommittee.index; - } - if (stakerInFutureCommittee.balance != stakerInCommitteeToOverwrite.balance) { - stakerInCommitteeToOverwrite.balance = stakerInFutureCommittee.balance; - } - // Otherwise remove them - } else { - delete committeeToOverwrite.stakers[stakerKey]; - stakerIndexChanged = true; - } - } - - // Now add any stakers which are in future committee which but not in our new committee - for ( - uint256 j = 0; - j < futureCommittee.stakerKeys.length; - j++ - ) { - bytes storage stakerKey = futureCommittee - .stakerKeys[j]; - if (committeeToOverwrite.stakers[stakerKey].index == 0) { - committeeToOverwrite.stakers[ - stakerKey - ] = futureCommittee.stakers[stakerKey]; - stakerIndexChanged = true; - } - } - - if (futureCommittee.totalStake != committeeToOverwrite.totalStake) { - committeeToOverwrite.totalStake = futureCommittee - .totalStake; - } - if (stakerIndexChanged) { - committeeToOverwrite.stakerKeys = futureCommittee - .stakerKeys; - } - - return committeeToOverwrite; - } - - function getFutureTotalStake() public view returns (uint256) { - return getFutureCommittee().totalStake; - } - - function getStakersData() - public - view - returns ( - bytes[] memory stakerKeys, - uint256[] memory indices, - uint256[] memory balances, - Staker[] memory stakers - ) - { - // TODO clean up doule call to _getDepositStorage() here - DepositStorage storage $ = _getDepositStorage(); - Committee storage currentCommittee = committee(); - - stakerKeys = currentCommittee.stakerKeys; - balances = new uint256[](stakerKeys.length); - stakers = new Staker[](stakerKeys.length); - for (uint256 i = 0; i < stakerKeys.length; i++) { - bytes memory key = stakerKeys[i]; - // The stakerKeys are not sorted by the stakers' - // index in the current committee, therefore we - // return the indices too, to help identify the - // stakers in the bit vectors stored along with - // BLS aggregate signatures - indices[i] = currentCommittee.stakers[key].index; - balances[i] = currentCommittee.stakers[key].balance; - stakers[i] = $._stakersMap[key]; - } - } - - function getStakerData( - bytes calldata blsPubKey - ) - public - view - returns (uint256 index, uint256 balance, Staker memory staker) - { - DepositStorage storage $ = _getDepositStorage(); - Committee storage currentCommittee = committee(); - index = currentCommittee.stakers[blsPubKey].index; - balance = currentCommittee.stakers[blsPubKey].balance; - staker = $._stakersMap[blsPubKey]; - } - - function getStake(bytes calldata blsPubKey) public view returns (uint256) { - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - - // We don't need to check if `blsPubKey` is in `stakerKeys` here. If the `blsPubKey` is not a staker, the - // balance will default to zero. - return committee().stakers[blsPubKey].balance; - } - - function getFutureStake( - bytes calldata blsPubKey - ) public view returns (uint256) { - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - - // We don't need to check if `blsPubKey` is in `stakerKeys` here. If the `blsPubKey` is not a staker, the - // balance will default to zero. - return getFutureCommittee().stakers[blsPubKey].balance; - } - - function getRewardAddress( - bytes calldata blsPubKey - ) public view returns (address) { - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - DepositStorage storage $ = _getDepositStorage(); - if ($._stakersMap[blsPubKey].controlAddress == address(0)) { - revert KeyNotStaked(); - } - return $._stakersMap[blsPubKey].rewardAddress; - } - - function getSigningAddress( - bytes calldata blsPubKey - ) public view returns (address) { - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - DepositStorage storage $ = _getDepositStorage(); - if ($._stakersMap[blsPubKey].controlAddress == address(0)) { - revert KeyNotStaked(); - } - address signingAddress = $._stakersMap[blsPubKey].signingAddress; - // If the staker was an InitialStaker on contract initialisation and have not called setSigningAddress() then there will be no signingAddress. - // Default to controlAddress to avoid revert - if (signingAddress == address(0)) { - signingAddress = $._stakersMap[blsPubKey].controlAddress; - } - return signingAddress; - } - - function getControlAddress( - bytes calldata blsPubKey - ) public view returns (address) { - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - DepositStorage storage $ = _getDepositStorage(); - if ($._stakersMap[blsPubKey].controlAddress == address(0)) { - revert KeyNotStaked(); - } - return $._stakersMap[blsPubKey].controlAddress; - } - - function setRewardAddress( - bytes calldata blsPubKey, - address rewardAddress - ) public onlyControlAddress(blsPubKey) { - DepositStorage storage $ = _getDepositStorage(); - $._stakersMap[blsPubKey].rewardAddress = rewardAddress; - } - - function setSigningAddress( - bytes calldata blsPubKey, - address signingAddress - ) public onlyControlAddress(blsPubKey) { - DepositStorage storage $ = _getDepositStorage(); - $._stakersMap[blsPubKey].signingAddress = signingAddress; - } - - function setControlAddress( - bytes calldata blsPubKey, - address controlAddress - ) public onlyControlAddress(blsPubKey) { - DepositStorage storage $ = _getDepositStorage(); - $._stakersMap[blsPubKey].controlAddress = controlAddress; - } - - function getPeerId( - bytes calldata blsPubKey - ) public view returns (bytes memory) { - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - DepositStorage storage $ = _getDepositStorage(); - if ($._stakersMap[blsPubKey].controlAddress == address(0)) { - revert KeyNotStaked(); - } - return $._stakersMap[blsPubKey].peerId; - } - - // Returns the next block number at which new stakers are added, - // existing ones removed and/or deposits of existing stakers change - function nextUpdate() public view returns (uint256 blockNumber) { - DepositStorage storage $ = _getDepositStorage(); - if ($.latestComputedEpoch > currentEpoch()) - blockNumber = $.latestComputedEpoch * $.blocksPerEpoch; - } - - // keep in-sync with zilliqa/src/precompiles.rs - function _blsVerify( - bytes memory message, - bytes memory pubkey, - bytes memory signature - ) internal view returns (bool) { - bytes memory input = abi.encodeWithSelector( - hex"a65ebb25", // bytes4(keccak256("blsVerify(bytes,bytes,bytes)")) - message, - signature, - pubkey - ); - uint256 inputLength = input.length; - bytes memory output = new bytes(32); - bool success; - assembly { - success := staticcall( - gas(), - 0x5a494c81, // "ZIL\x81" - add(input, 0x20), - inputLength, - add(output, 0x20), - 32 - ) - } - require(success, "blsVerify"); - bool result = abi.decode(output, (bool)); - return result; - } - - function deposit( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature, - address rewardAddress, - address signingAddress - ) public payable { - if (blsPubKey.length != 48) { - revert UnexpectedArgumentLength("bls public key", 48); - } - if (peerId.length != 38) { - revert UnexpectedArgumentLength("peer id", 38); - } - if (signature.length != 96) { - revert UnexpectedArgumentLength("signature", 96); - } - DepositStorage storage $ = _getDepositStorage(); - - bytes memory message = abi.encodePacked( - blsPubKey, - uint64(block.chainid), - msg.sender - ); - - // Verify bls signature - if (!_blsVerify(message, blsPubKey, signature)) { - revert RogueKeyCheckFailed(); - } - - if (msg.value < $.minimumStake) { - revert StakeAmountTooLow(); - } - - Staker storage staker = $._stakersMap[blsPubKey]; - staker.peerId = peerId; - staker.rewardAddress = rewardAddress; - staker.signingAddress = signingAddress; - staker.controlAddress = msg.sender; - - Committee storage futureCommittee = getOrCreateFutureCommittee(); - - if (futureCommittee.stakerKeys.length >= $.maximumStakers) { - revert TooManyStakers(); - } - if (futureCommittee.stakers[blsPubKey].index != 0) { - revert KeyAlreadyStaked(); - } - - futureCommittee.totalStake += msg.value; - futureCommittee.stakers[blsPubKey].balance = msg.value; - futureCommittee.stakers[blsPubKey].index = - futureCommittee.stakerKeys.length + - 1; - futureCommittee.stakerKeys.push(blsPubKey); - - emit StakerAdded(blsPubKey, nextUpdate(), msg.value); - } - - function depositTopup(bytes calldata blsPubKey) public payable onlyControlAddress(blsPubKey) { - Committee storage futureCommittee = getFutureCommittee(); - if (futureCommittee.stakers[blsPubKey].index == 0) { - revert KeyNotStaked(); - } - - futureCommittee.totalStake += msg.value; - futureCommittee.stakers[blsPubKey].balance += msg.value; - - emit StakeChanged( - blsPubKey, - nextUpdate(), - futureCommittee.stakers[blsPubKey].balance - ); - } - - function unstake(bytes calldata blsPubKey, uint256 amount) public onlyControlAddress(blsPubKey) { - DepositStorage storage $ = _getDepositStorage(); - - - Committee storage futureCommittee = getFutureCommittee(); - if (futureCommittee.stakers[blsPubKey].index == 0) { - revert KeyNotStaked(); - } - - require( - futureCommittee.stakers[blsPubKey].balance >= amount, - "amount is greater than staked balance" - ); - - if (futureCommittee.stakers[blsPubKey].balance - amount == 0) { - require(futureCommittee.stakerKeys.length > 1, "too few stakers"); - - // Remove the staker from the future committee, because their staked amount has gone to zero. - futureCommittee.totalStake -= amount; - - uint256 deleteIndex = futureCommittee.stakers[blsPubKey].index - 1; - uint256 lastIndex = futureCommittee.stakerKeys.length - 1; - - if (deleteIndex != lastIndex) { - // Move the last staker in `stakerKeys` to the position of the staker we want to delete. - bytes storage lastStakerKey = futureCommittee.stakerKeys[ - lastIndex - ]; - futureCommittee.stakerKeys[deleteIndex] = lastStakerKey; - // We need to remember to update the moved staker's `index` too. - futureCommittee.stakers[lastStakerKey].index = futureCommittee - .stakers[blsPubKey] - .index; - } - - // It is now safe to delete the final staker in the list. - futureCommittee.stakerKeys.pop(); - delete futureCommittee.stakers[blsPubKey]; - - // Note that we leave the staker in `_stakersMap` forever. - - emit StakerRemoved(blsPubKey, nextUpdate()); - } else { - require( - futureCommittee.stakers[blsPubKey].balance - amount >= - $.minimumStake, - "unstaking this amount would take the validator below the minimum stake" - ); - - // Partial unstake. The staker stays in the committee, but with a reduced stake. - futureCommittee.totalStake -= amount; - futureCommittee.stakers[blsPubKey].balance -= amount; - - emit StakeChanged( - blsPubKey, - nextUpdate(), - futureCommittee.stakers[blsPubKey].balance - ); - } - - // Enqueue the withdrawal for this staker. - Deque.Withdrawals storage withdrawals = $._stakersMap[blsPubKey].withdrawals; - Withdrawal storage currentWithdrawal; - // We know `withdrawals` is sorted by `startedAt`. We also know `block.number` is monotonically - // non-decreasing. Therefore if there is an existing entry with a `startedAt = block.number`, it must be - // at the end of the queue. - if ( - withdrawals.length() != 0 && - withdrawals.back().startedAt == block.number - ) { - // They have already made a withdrawal at this time, so grab a reference to the existing one. - currentWithdrawal = withdrawals.back(); - } else { - // Add a new withdrawal to the end of the queue. - currentWithdrawal = withdrawals.pushBack(); - currentWithdrawal.startedAt = block.number; - currentWithdrawal.amount = 0; - } - currentWithdrawal.amount += amount; - } - - function withdraw(bytes calldata blsPubKey) public { - _withdraw(blsPubKey, 0); - } - - function withdraw(bytes calldata blsPubKey, uint256 count) public { - _withdraw(blsPubKey, count); - } - - /// Unbonding period for withdrawals measured in number of blocks (note that we have 1 second block times) - function withdrawalPeriod() public view returns (uint256) { - // shorter unbonding period for testing deposit withdrawals - if (block.chainid == 33469) return 5 minutes; - return 2 weeks; - } - - function _withdraw(bytes calldata blsPubKey, uint256 count) internal onlyControlAddress(blsPubKey) { - DepositStorage storage $ = _getDepositStorage(); - - uint256 releasedAmount = 0; - - Deque.Withdrawals storage withdrawals = $._stakersMap[blsPubKey].withdrawals; - count = (count == 0 || count > withdrawals.length()) - ? withdrawals.length() - : count; - - while (count > 0) { - Withdrawal storage withdrawal = withdrawals.front(); - if (withdrawal.startedAt + withdrawalPeriod() <= block.number) { - releasedAmount += withdrawal.amount; - withdrawals.popFront(); - } else { - // Thanks to the invariant on `withdrawals`, we know the elements are ordered by `startedAt`, so we can - // break early when we encounter any withdrawal that isn't ready to be released yet. - break; - } - count -= 1; - } - - (bool sent, ) = msg.sender.call{value: releasedAmount}(""); - require(sent, "failed to send"); - } -} \ No newline at end of file