From 7dea97504feeb44646aab5a998ed67bc7ad5084f Mon Sep 17 00:00:00 2001 From: Tomos Wootton Date: Tue, 7 Jan 2025 11:32:28 +0000 Subject: [PATCH 1/6] chore: init branch with v copy in deposit_v4 --- zilliqa/src/contracts/deposit_v4.sol | 698 +++++++++++++++++++++++++++ 1 file changed, 698 insertions(+) create mode 100644 zilliqa/src/contracts/deposit_v4.sol diff --git a/zilliqa/src/contracts/deposit_v4.sol b/zilliqa/src/contracts/deposit_v4.sol new file mode 100644 index 000000000..0fa429d1b --- /dev/null +++ b/zilliqa/src/contracts/deposit_v4.sol @@ -0,0 +1,698 @@ +// 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); + +/// 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; +} + +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 committee in the current epoch and the 2 epochs following it. The value for the current epoch + // is stored at index (currentEpoch() % 3). + Committee[3] _committee; + // All stakers. Keys into this map are stored by the `Committee`. + mapping(bytes => Staker) _stakersMap; + // Mapping from `controlAddress` to `blsPubKey` for each staker. + mapping(address => bytes) _stakerKeys; + // 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); + } + require( + $._stakersMap[blsPubKey].controlAddress == msg.sender, + "sender is not the control address" + ); + _; + } + + // 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(); + 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]; + } + } + + 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 getFutureTotalStake() public view returns (uint256) { + 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; + } + + 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); + } + 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; + } + + 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; + delete $._stakerKeys[msg.sender]; + $._stakerKeys[controlAddress] = blsPubKey; + } + + 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; + } + + 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 need to do it + // explicitly because `stakers` is a mapping. + + // Delete old keys from `_committee[i % 3].stakers`. + for ( + uint256 j = 0; + j < $._committee[i % 3].stakerKeys.length; + j++ + ) { + delete $._committee[i % 3].stakers[ + $._committee[i % 3].stakerKeys[j] + ]; + } + + $._committee[i % 3].totalStake = latestComputedCommittee + .totalStake; + $._committee[i % 3].stakerKeys = latestComputedCommittee + .stakerKeys; + for ( + uint256 j = 0; + j < latestComputedCommittee.stakerKeys.length; + j++ + ) { + bytes storage stakerKey = latestComputedCommittee + .stakerKeys[j]; + $._committee[i % 3].stakers[ + stakerKey + ] = latestComputedCommittee.stakers[stakerKey]; + } + } + + $.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; + } + + // 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(); + } + + $._stakerKeys[msg.sender] = blsPubKey; + Staker storage staker = $._stakersMap[blsPubKey]; + staker.peerId = peerId; + staker.rewardAddress = rewardAddress; + staker.signingAddress = signingAddress; + staker.controlAddress = msg.sender; + + updateLatestComputedEpoch(); + + Committee storage futureCommittee = $._committee[ + (currentEpoch() + 2) % 3 + ]; + + 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() public payable { + DepositStorage storage $ = _getDepositStorage(); + bytes storage stakerKey = $._stakerKeys[msg.sender]; + if (stakerKey.length == 0) { + revert KeyNotStaked(); + } + + updateLatestComputedEpoch(); + + Committee storage futureCommittee = $._committee[ + (currentEpoch() + 2) % 3 + ]; + if (futureCommittee.stakers[stakerKey].index == 0) { + revert KeyNotStaked(); + } + futureCommittee.totalStake += msg.value; + futureCommittee.stakers[stakerKey].balance += msg.value; + + emit StakeChanged( + stakerKey, + nextUpdate(), + futureCommittee.stakers[stakerKey].balance + ); + } + + function unstake(uint256 amount) public { + DepositStorage storage $ = _getDepositStorage(); + bytes storage stakerKey = $._stakerKeys[msg.sender]; + if (stakerKey.length == 0) { + revert KeyNotStaked(); + } + Staker storage staker = $._stakersMap[stakerKey]; + + updateLatestComputedEpoch(); + + Committee storage futureCommittee = $._committee[ + (currentEpoch() + 2) % 3 + ]; + if (futureCommittee.stakers[stakerKey].index == 0) { + revert KeyNotStaked(); + } + + require( + futureCommittee.stakers[stakerKey].balance >= amount, + "amount is greater than staked balance" + ); + + if (futureCommittee.stakers[stakerKey].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[stakerKey].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[stakerKey] + .index; + } + + // It is now safe to delete the final staker in the list. + futureCommittee.stakerKeys.pop(); + delete futureCommittee.stakers[stakerKey]; + + // Note that we leave the staker in `_stakersMap` forever. + + emit StakerRemoved(stakerKey, nextUpdate()); + } else { + require( + futureCommittee.stakers[stakerKey].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[stakerKey].balance -= amount; + + emit StakeChanged( + stakerKey, + nextUpdate(), + futureCommittee.stakers[stakerKey].balance + ); + } + + // Enqueue the withdrawal for this staker. + Deque.Withdrawals storage withdrawals = staker.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() public { + _withdraw(0); + } + + function withdraw(uint256 count) public { + _withdraw(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(uint256 count) internal { + uint256 releasedAmount = 0; + + DepositStorage storage $ = _getDepositStorage(); + Staker storage staker = $._stakersMap[$._stakerKeys[msg.sender]]; + + Deque.Withdrawals storage withdrawals = staker.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"); + } +} From 8cd7e0e1e8657ad0b4d54285f185a82b0341e9e2 Mon Sep 17 00:00:00 2001 From: tomos <55086152+86667@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:40:15 +0000 Subject: [PATCH 2/6] Remove _stakerKeys from Deposit contract (#2105) * build(deps): bump foundry-compilers from 0.12.8 to 0.12.9 (#2096) Bumps [foundry-compilers](https://github.com/foundry-rs/compilers) from 0.12.8 to 0.12.9. - [Changelog](https://github.com/foundry-rs/compilers/blob/main/CHANGELOG.md) - [Commits](https://github.com/foundry-rs/compilers/compare/v0.12.8...v0.12.9) --- updated-dependencies: - dependency-name: foundry-compilers dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: remove _stakerKeys from deposit contract * fix: use onlyControlAddress() --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 51 +++++++++--------- zilliqa/Cargo.toml | 2 +- zilliqa/src/contracts/deposit_v4.sol | 77 ++++++++++++---------------- 3 files changed, 59 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c10768bf..434287903 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" dependencies = [ "anstyle", + "memchr", "unicode-width 0.2.0", ] @@ -3114,9 +3115,9 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d817beee8c566a99f4267f25ff63d0de46c442948496ecef91ead56e3383090c" +checksum = "f67e3eab56847dcf269eb186226f95874b171e262952cff6c910da36b1469e10" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -3146,9 +3147,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec784a3a809ba2ee723fcfeb737a6ac90b4fd1e4d048c2d49fed6723bd35547" +checksum = "865b00448dc2a5d56bae287c36fa716379ffcdd937aefb7758bd20b62024d234" dependencies = [ "foundry-compilers-artifacts-solc", "foundry-compilers-artifacts-vyper", @@ -3156,9 +3157,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-solc" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44549c33e5a03408c8d40c36d764b7e84d261258ef481c19e4a612e609fdf8a4" +checksum = "668972ba511f80895ea12c75cd12fccd6627c26e64763799d83978b4e0916cae" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -3178,9 +3179,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-vyper" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a438605ae74689752b2f717165daac15766f1b2a166d2095715d5f9407084b52" +checksum = "5a24f7f2a7458171e055c0cb33272f5eccaefbd96d791d74177d9a1fca048f74" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -3193,9 +3194,9 @@ dependencies = [ [[package]] name = "foundry-compilers-core" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04ac6d85c3e2d12585f8e698b12ed4880b02716ec7fde5d62de9a194e62f4e36" +checksum = "8005271a079bc6470c61d4145d2e390a827b1ccbb96abb7b69b088f17ffb95e0" dependencies = [ "alloy-primitives", "cfg-if", @@ -8231,9 +8232,9 @@ dependencies = [ [[package]] name = "solar-ast" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aeaf7a4bd326242c909bd287291226a540b62b36fa5824880248f4b1d4d6af" +checksum = "5d3f6c4a476a16dcd36933a70ecdb0a807f8949cc5f3c4c1984e3748666bd714" dependencies = [ "alloy-primitives", "bumpalo", @@ -8250,18 +8251,18 @@ dependencies = [ [[package]] name = "solar-config" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d00d672a40a1a3620d7696f01a2d3301abf883d8168e1a9da3bf83f0c8e343" +checksum = "d40434a61f2c14a9e3777fbc478167bddee9828532fc26c57e416e9277916b09" dependencies = [ "strum", ] [[package]] name = "solar-data-structures" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6e4eb0b72ed7adbb808897c85de08ea99609774a58c72e3dce55c758043ca2" +checksum = "71d07263243b313296eca18f18eda3a190902dc3284bf67ceff29b8b54dac3e6" dependencies = [ "bumpalo", "index_vec", @@ -8274,9 +8275,9 @@ dependencies = [ [[package]] name = "solar-interface" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21fb8925638f3da1bba7a9a6ebeac3511e5c6354f921f2bb2e1ddce4ac70c107" +checksum = "9a87009b6989b2cc44d8381e3b86ff3b90280d54a60321919b6416214cd602f3" dependencies = [ "annotate-snippets", "anstream", @@ -8284,7 +8285,7 @@ dependencies = [ "const-hex", "derive_builder", "dunce", - "itertools 0.13.0", + "itertools 0.14.0", "itoa", "lasso", "match_cfg", @@ -8295,16 +8296,16 @@ dependencies = [ "solar-config", "solar-data-structures", "solar-macros", - "thiserror 1.0.69", + "thiserror 2.0.9", "tracing", "unicode-width 0.2.0", ] [[package]] name = "solar-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0cc54b74e214647c1bbfc098d080cc5deac77f8dcb99aca91747276b01a15ad" +checksum = "970d7c774741f786d62cab78290e47d845b0b9c0c9d094a1642aced1d7946036" dependencies = [ "proc-macro2", "quote", @@ -8313,14 +8314,14 @@ dependencies = [ [[package]] name = "solar-parse" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82c3659c15975cd80e5e1c44591278c230c59ad89082d797837499a4784e1b" +checksum = "2e1e2d07fae218aca1b4cca81216e5c9ad7822516d48a28f11e2eaa8ffa5b249" dependencies = [ "alloy-primitives", "bitflags 2.6.0", "bumpalo", - "itertools 0.13.0", + "itertools 0.14.0", "memchr", "num-bigint", "num-rational", diff --git a/zilliqa/Cargo.toml b/zilliqa/Cargo.toml index 2b7b57e37..3ed513c9f 100644 --- a/zilliqa/Cargo.toml +++ b/zilliqa/Cargo.toml @@ -91,7 +91,7 @@ alloy = { version = "0.6.4", default-features = false, features = ["network", "r async-trait = "0.1.84" criterion = "0.5.1" ethers = { version = "2.0.14", default-features = false, features = ["legacy"] } -foundry-compilers = { version = "0.12.8", features = ["svm-solc"] } +foundry-compilers = { version = "0.12.9", features = ["svm-solc"] } fs_extra = "1.3.0" indicatif = { version = "0.17.9", features = ["rayon"] } pprof = { version = "0.14.0", default-features = false, features = ["criterion", "flamegraph"] } diff --git a/zilliqa/src/contracts/deposit_v4.sol b/zilliqa/src/contracts/deposit_v4.sol index 0fa429d1b..6864c5171 100644 --- a/zilliqa/src/contracts/deposit_v4.sol +++ b/zilliqa/src/contracts/deposit_v4.sol @@ -11,6 +11,8 @@ using Deque for Deque.Withdrawals; /// @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 @@ -84,8 +86,6 @@ contract Deposit is UUPSUpgradeable { Committee[3] _committee; // All stakers. Keys into this map are stored by the `Committee`. mapping(bytes => Staker) _stakersMap; - // Mapping from `controlAddress` to `blsPubKey` for each staker. - mapping(address => bytes) _stakerKeys; // 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. @@ -100,10 +100,9 @@ contract Deposit is UUPSUpgradeable { if (blsPubKey.length != 48) { revert UnexpectedArgumentLength("bls public key", 48); } - require( - $._stakersMap[blsPubKey].controlAddress == msg.sender, - "sender is not the control address" - ); + if ($._stakersMap[blsPubKey].controlAddress != msg.sender) { + revert Unauthorised(); + } _; } @@ -369,8 +368,6 @@ contract Deposit is UUPSUpgradeable { ) public onlyControlAddress(blsPubKey) { DepositStorage storage $ = _getDepositStorage(); $._stakersMap[blsPubKey].controlAddress = controlAddress; - delete $._stakerKeys[msg.sender]; - $._stakerKeys[controlAddress] = blsPubKey; } function getPeerId( @@ -509,7 +506,6 @@ contract Deposit is UUPSUpgradeable { revert StakeAmountTooLow(); } - $._stakerKeys[msg.sender] = blsPubKey; Staker storage staker = $._stakersMap[blsPubKey]; staker.peerId = peerId; staker.rewardAddress = rewardAddress; @@ -539,60 +535,52 @@ contract Deposit is UUPSUpgradeable { emit StakerAdded(blsPubKey, nextUpdate(), msg.value); } - function depositTopup() public payable { + 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 ]; - if (futureCommittee.stakers[stakerKey].index == 0) { + if (futureCommittee.stakers[blsPubKey].index == 0) { revert KeyNotStaked(); } + futureCommittee.totalStake += msg.value; - futureCommittee.stakers[stakerKey].balance += msg.value; + futureCommittee.stakers[blsPubKey].balance += msg.value; emit StakeChanged( - stakerKey, + blsPubKey, nextUpdate(), - futureCommittee.stakers[stakerKey].balance + futureCommittee.stakers[blsPubKey].balance ); } - function unstake(uint256 amount) public { + function unstake(bytes calldata blsPubKey, uint256 amount) public onlyControlAddress(blsPubKey) { DepositStorage storage $ = _getDepositStorage(); - bytes storage stakerKey = $._stakerKeys[msg.sender]; - if (stakerKey.length == 0) { - revert KeyNotStaked(); - } - Staker storage staker = $._stakersMap[stakerKey]; updateLatestComputedEpoch(); Committee storage futureCommittee = $._committee[ (currentEpoch() + 2) % 3 ]; - if (futureCommittee.stakers[stakerKey].index == 0) { + if (futureCommittee.stakers[blsPubKey].index == 0) { revert KeyNotStaked(); } require( - futureCommittee.stakers[stakerKey].balance >= amount, + futureCommittee.stakers[blsPubKey].balance >= amount, "amount is greater than staked balance" ); - if (futureCommittee.stakers[stakerKey].balance - amount == 0) { + 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[stakerKey].index - 1; + uint256 deleteIndex = futureCommittee.stakers[blsPubKey].index - 1; uint256 lastIndex = futureCommittee.stakerKeys.length - 1; if (deleteIndex != lastIndex) { @@ -603,37 +591,37 @@ contract Deposit is UUPSUpgradeable { futureCommittee.stakerKeys[deleteIndex] = lastStakerKey; // We need to remember to update the moved staker's `index` too. futureCommittee.stakers[lastStakerKey].index = futureCommittee - .stakers[stakerKey] + .stakers[blsPubKey] .index; } // It is now safe to delete the final staker in the list. futureCommittee.stakerKeys.pop(); - delete futureCommittee.stakers[stakerKey]; + delete futureCommittee.stakers[blsPubKey]; // Note that we leave the staker in `_stakersMap` forever. - emit StakerRemoved(stakerKey, nextUpdate()); + emit StakerRemoved(blsPubKey, nextUpdate()); } else { require( - futureCommittee.stakers[stakerKey].balance - amount >= + 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[stakerKey].balance -= amount; + futureCommittee.stakers[blsPubKey].balance -= amount; emit StakeChanged( - stakerKey, + blsPubKey, nextUpdate(), - futureCommittee.stakers[stakerKey].balance + futureCommittee.stakers[blsPubKey].balance ); } // Enqueue the withdrawal for this staker. - Deque.Withdrawals storage withdrawals = staker.withdrawals; + 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 @@ -653,12 +641,12 @@ contract Deposit is UUPSUpgradeable { currentWithdrawal.amount += amount; } - function withdraw() public { - _withdraw(0); + function withdraw(bytes calldata blsPubKey) public { + _withdraw(blsPubKey, 0); } - function withdraw(uint256 count) public { - _withdraw(count); + 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) @@ -668,13 +656,12 @@ contract Deposit is UUPSUpgradeable { return 2 weeks; } - function _withdraw(uint256 count) internal { - uint256 releasedAmount = 0; - + function _withdraw(bytes calldata blsPubKey, uint256 count) internal onlyControlAddress(blsPubKey) { DepositStorage storage $ = _getDepositStorage(); - Staker storage staker = $._stakersMap[$._stakerKeys[msg.sender]]; - Deque.Withdrawals storage withdrawals = staker.withdrawals; + uint256 releasedAmount = 0; + + Deque.Withdrawals storage withdrawals = $._stakersMap[blsPubKey].withdrawals; count = (count == 0 || count > withdrawals.length()) ? withdrawals.length() : count; From 1d48d4d8408de7ccca2a2c8d6de9d1026dd56590 Mon Sep 17 00:00:00 2001 From: Tomos Wootton Date: Wed, 8 Jan 2025 17:13:39 +0000 Subject: [PATCH 3/6] feat: deposit_v4 with cheaper depositTopUp --- zilliqa/src/contracts/deposit_v4.sol | 58 +++++++++++++++++++++------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/zilliqa/src/contracts/deposit_v4.sol b/zilliqa/src/contracts/deposit_v4.sol index 6864c5171..35c3b5df5 100644 --- a/zilliqa/src/contracts/deposit_v4.sol +++ b/zilliqa/src/contracts/deposit_v4.sol @@ -400,24 +400,36 @@ contract Deposit is UUPSUpgradeable { i <= currentEpoch() + 2 && i < $.latestComputedEpoch + 3; i++ ) { - // The operation we want to do is: `_committee[i % 3] = latestComputedCommittee` but we need to do it - // explicitly because `stakers` is a mapping. + // 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; - // Delete old keys from `_committee[i % 3].stakers`. + // Overwrite existing staker's data for ( uint256 j = 0; - j < $._committee[i % 3].stakerKeys.length; + j < committeeToUpdate.stakerKeys.length; j++ ) { - delete $._committee[i % 3].stakers[ - $._committee[i % 3].stakerKeys[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; + } } - $._committee[i % 3].totalStake = latestComputedCommittee - .totalStake; - $._committee[i % 3].stakerKeys = latestComputedCommittee - .stakerKeys; + // Now add any new stakers for ( uint256 j = 0; j < latestComputedCommittee.stakerKeys.length; @@ -425,9 +437,21 @@ contract Deposit is UUPSUpgradeable { ) { bytes storage stakerKey = latestComputedCommittee .stakerKeys[j]; - $._committee[i % 3].stakers[ - stakerKey - ] = latestComputedCommittee.stakers[stakerKey]; + 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; } } @@ -533,11 +557,17 @@ 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[ From a73922a3c696426a6f4628472d5186bd9b25e075 Mon Sep 17 00:00:00 2001 From: Tomos Wootton Date: Wed, 8 Jan 2025 09:52:38 +0000 Subject: [PATCH 4/6] feat: deposit_v5 --- zilliqa/src/contracts/deposit_v5.sol | 705 +++++++++++++++++++++++++++ 1 file changed, 705 insertions(+) create mode 100644 zilliqa/src/contracts/deposit_v5.sol diff --git a/zilliqa/src/contracts/deposit_v5.sol b/zilliqa/src/contracts/deposit_v5.sol new file mode 100644 index 000000000..51f54e482 --- /dev/null +++ b/zilliqa/src/contracts/deposit_v5.sol @@ -0,0 +1,705 @@ +// 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 From 575142bd1d0af9086b7b01a5b4c6369d4511e14b Mon Sep 17 00:00:00 2001 From: Tomos Wootton Date: Wed, 8 Jan 2025 18:19:39 +0000 Subject: [PATCH 5/6] feat: replace deposit_v4 with deposit_v5 code --- zilliqa/src/contracts/deposit_v4.sol | 254 +++++----- zilliqa/src/contracts/deposit_v5.sol | 705 --------------------------- 2 files changed, 121 insertions(+), 838 deletions(-) delete mode 100644 zilliqa/src/contracts/deposit_v5.sol diff --git a/zilliqa/src/contracts/deposit_v4.sol b/zilliqa/src/contracts/deposit_v4.sol index 35c3b5df5..9a11a4259 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 larger 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 = getOrCreateFutureCommittee(); 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 = getOrCreateFutureCommittee(); 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 From 3c8e9bd765d105858a6a6d28ac90b821f31b801a Mon Sep 17 00:00:00 2001 From: Tomos Wootton Date: Thu, 9 Jan 2025 15:49:09 +0000 Subject: [PATCH 6/6] feat: all committee calculations in memory --- zilliqa/src/contracts/deposit_v5.sol | 764 +++++++++++++++++++++++++++ 1 file changed, 764 insertions(+) create mode 100644 zilliqa/src/contracts/deposit_v5.sol diff --git a/zilliqa/src/contracts/deposit_v5.sol b/zilliqa/src/contracts/deposit_v5.sol new file mode 100644 index 000000000..2c319621d --- /dev/null +++ b/zilliqa/src/contracts/deposit_v5.sol @@ -0,0 +1,764 @@ +// 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; + // Currently active balance + // Invariant: `balance >= minimumStake` + uint256 balance; +} + +struct Committee { + // Invariant: Equal to the sum of `balances` in `stakers`. + uint256 totalStake; + bytes[] stakerKeys; +} + +struct BalanceUpdate { + int256 amount; + // The epoch in which this balance update becomes active + uint256 epoch; +} + +struct Balance { + uint256 active; + BalanceUpdate[] pending; +} + +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; + // Balance including pending updates in upcoming epochs + Balance balance; + // The index of the value in the `stakers` array plus 1. + // Index 0 is used to mean a value is not present. + uint256 index; +} + +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); + + // Emitted to inform that a stakers position in the list of stakers (committee.stakerKeys) has changed + event StakerMoved(bytes blsPubKey, uint256 newPosition, uint256 atFutureBlock); + + uint64 public constant VERSION = 3; + + /// @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). + Committee[3] _committee; + // All stakers. Keys into this map are stored by the `Committee`. + mapping(bytes => Staker) _stakersMap; + // Keys of items in _stakersMap + bytes[] _stakersKeys; + // 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(bool includeAll) private view returns (Committee memory) { + DepositStorage storage $ = _getDepositStorage(); + Committee memory currentCommittee; + uint256 currentEpochVal = currentEpoch(); + uint8 numAdditions = 0; + + for ( + uint256 i = 0; + i < $._stakersKeys.length; + i++ + ) { + bytes memory stakerKey = $._stakersKeys[i]; + uint256 balance = getStakersBalance(stakerKey, includeAll); + // If staker has active balance then add them to the committee + if (balance > 0) { + numAdditions += 1; + currentCommittee.totalStake += uint256(balance); + } + } + + currentCommittee.stakerKeys = new bytes[](numAdditions); + uint8 numAdded = 0; + for ( + uint256 i = 0; + i < $._stakersKeys.length; + i++ + ) { + bytes memory stakerKey = $._stakersKeys[i]; + uint256 balance = getStakersBalance(stakerKey, includeAll); + // If staker has active balance then add them to the committee + if (balance > 0) { + currentCommittee.stakerKeys[numAdded] = stakerKey; + numAdded += 1; + } + } + + return currentCommittee; + } + + function getStakersBalance(bytes memory stakerKey, bool includeAll) private view returns (uint256) { + DepositStorage storage $ = _getDepositStorage(); + uint256 currentEpochVal = currentEpoch(); + + Staker memory staker = $._stakersMap[stakerKey]; + if (staker.controlAddress == address(0)) { + return uint256(0); + } + + uint256 balance = staker.balance.active; + + // Add any pending balance updates which may now be considered active + for ( + uint256 i = 0; + i < staker.balance.pending.length; + i++ + ) { + // Include all balance updates or only those currenlty active + if (includeAll || staker.balance.pending[i].epoch <= currentEpochVal) { + if (staker.balance.pending[i].amount > 0) { + balance += uint256(staker.balance.pending[i].amount); + } else { + // we ensure balance never goes below 0 in unstake() + balance -= uint256(-staker.balance.pending[i].amount); + } + } + } + return balance; + } + + function foldStakersBalance(bytes memory stakerKey) private { + DepositStorage storage $ = _getDepositStorage(); + uint256 currentEpochVal = currentEpoch(); + + Staker memory staker = $._stakersMap[stakerKey]; + uint256 balance = staker.balance.active; + uint256 removePendingBalancesUptoIndex; + // Add any pending balance updates which may now be considered active + for ( + uint256 i = 0; + i < staker.balance.pending.length; + i++ + ) { + // Include all balance updates or only those currently active + if (staker.balance.pending[i].epoch <= currentEpochVal) { + if (staker.balance.pending[i].amount > 0) { + balance += uint256(staker.balance.pending[i].amount); + } else { + // we ensure balance never goes below 0 in unstake() + balance -= uint256(-staker.balance.pending[i].amount); + } + removePendingBalancesUptoIndex = i + 1; + } + } + // Pending balance array is in order of epochs. Ie all items in epoch x will be before items in epoch x+1 + // Therefore to remove items we can take a slice of the array and copy the result back into storage + if (removePendingBalancesUptoIndex > 0) { + BalanceUpdate[] memory slice = new BalanceUpdate[](staker.balance.pending.length - removePendingBalancesUptoIndex); + for (uint256 i = removePendingBalancesUptoIndex; i < staker.balance.pending.length; i++) { + slice[i - removePendingBalancesUptoIndex] = staker.balance.pending[i]; + } + + // Write pending values to balance if they are now active + staker.balance.active = balance; + staker.balance.pending = slice; + } + } + + 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) { + DepositStorage storage $ = _getDepositStorage(); + Committee memory currentCommittee = committee(false); + // 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 = getStakersBalance(stakerKey, false); + + 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(false).stakerKeys; + } + + function getTotalStake() public view returns (uint256) { + return committee(false).totalStake; + } + + function getFutureTotalStake() public view returns (uint256) { + return committee(true).totalStake; + } + + function getStakersData() + public + view + returns ( + bytes[] memory stakerKeys, + uint256[] memory indices, + uint256[] memory balances, + Staker[] memory stakers + ) + { + // TODO clean up double call to _getDepositStorage() here + DepositStorage storage $ = _getDepositStorage(); + Committee memory currentCommittee = committee(false); + + 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] = i + 1; + balances[i] = getStakersBalance(key, false); + stakers[i] = $._stakersMap[key]; + } + } + + function getStakerData( + bytes calldata blsPubKey + ) + public + view + returns (uint256 index, uint256 balance, Staker memory staker) + { + if (blsPubKey.length != 48) { + revert UnexpectedArgumentLength("bls public key", 48); + } + DepositStorage storage $ = _getDepositStorage(); + Committee memory currentCommittee = committee(false); + + for (uint256 i = 0; i < currentCommittee.stakerKeys.length; i++) { + if (compareBytes(blsPubKey, currentCommittee.stakerKeys[i])) { + index = i; + } + } + balance = getStakersBalance(blsPubKey, false); + staker = $._stakersMap[blsPubKey]; + } + + function compareBytes(bytes memory bytes_1, bytes memory bytes_2) private view returns (bool) { + if (bytes_1.length != bytes_2.length) { + return false; + } + for (uint i = 0; i < bytes_1.length; i++) { + if (bytes_1[i] != bytes_2[i]) { + return false; + } + } + return true; + } + + 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 getStakersBalance(blsPubKey, false); + } + + 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 getStakersBalance(blsPubKey, true); + } + + 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; + emit StakerUpdated(blsPubKey); + } + + function setSigningAddress( + bytes calldata blsPubKey, + address signingAddress + ) public onlyControlAddress(blsPubKey) { + DepositStorage storage $ = _getDepositStorage(); + $._stakersMap[blsPubKey].signingAddress = signingAddress; + emit StakerUpdated(blsPubKey); + } + + function setControlAddress( + bytes calldata blsPubKey, + address controlAddress + ) public onlyControlAddress(blsPubKey) { + DepositStorage storage $ = _getDepositStorage(); + $._stakersMap[blsPubKey].controlAddress = controlAddress; + emit StakerUpdated(blsPubKey); + } + + 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(); + } + + if (getStakersBalance(blsPubKey, true) != 0) { + revert KeyAlreadyStaked(); + } + + Staker storage staker = $._stakersMap[blsPubKey]; + staker.peerId = peerId; + staker.rewardAddress = rewardAddress; + staker.signingAddress = signingAddress; + staker.controlAddress = msg.sender; + staker.balance.pending.push(BalanceUpdate({ + amount:int256(msg.value), + epoch: currentEpoch() + 2 + })); + + Committee memory futureCommittee = committee(true); + if (futureCommittee.stakerKeys.length >= $.maximumStakers) { + revert TooManyStakers(); + } + + $.latestComputedEpoch = currentEpoch() + 2; + $._stakersKeys.push(blsPubKey); + + emit StakerAdded(blsPubKey, nextUpdate(), msg.value); + } + + function depositTopup(bytes calldata blsPubKey) public payable onlyControlAddress(blsPubKey) { + DepositStorage storage $ = _getDepositStorage(); + foldStakersBalance(blsPubKey); + + uint256 currentBalance = getStakersBalance(blsPubKey, true); + if (currentBalance == 0) { + revert KeyNotStaked(); + } + + Staker storage staker = $._stakersMap[blsPubKey]; + staker.balance.pending.push(BalanceUpdate({ + amount:int256(msg.value), + epoch: currentEpoch() + 2 + })); + $.latestComputedEpoch = currentEpoch() + 2; + + emit StakeChanged( + blsPubKey, + nextUpdate(), + currentBalance + msg.value + ); + } + + function unstake(bytes calldata blsPubKey, uint256 amount) public onlyControlAddress(blsPubKey) { + DepositStorage storage $ = _getDepositStorage(); + foldStakersBalance(blsPubKey); + + Staker storage staker = $._stakersMap[blsPubKey]; + uint256 currentBalance = getStakersBalance(blsPubKey, true); + + if (currentBalance == 0) { + revert KeyNotStaked(); + } + + require( + currentBalance >= amount, + "amount is greater than staked balance" + ); + + staker.balance.pending.push(BalanceUpdate({ + amount: -int256(amount), + epoch: currentEpoch() + 2 + })); + $.latestComputedEpoch = currentEpoch() + 2; + + Committee memory futureCommittee = committee(true); + + if (currentBalance - 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; + // TODO deal with this. Indices different now + // emit StakerMoved(lastStakerKey, deleteIndex, nextUpdate()); + // } + + // // 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( + currentBalance - 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(), + currentBalance - amount + ); + } + + + // 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"); + } +}