From f345bda3e9fdbd426d63f1a78dec2f69459afcf0 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 20 Mar 2024 10:39:12 +0100 Subject: [PATCH 1/6] feat: multiprover draft contract --- contracts/0.8.9/oracle/Multiprover.sol | 224 +++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 contracts/0.8.9/oracle/Multiprover.sol diff --git a/contracts/0.8.9/oracle/Multiprover.sol b/contracts/0.8.9/oracle/Multiprover.sol new file mode 100644 index 000000000..1d0296a7c --- /dev/null +++ b/contracts/0.8.9/oracle/Multiprover.sol @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol"; + +interface LidoZKOracle { + function getReport(uint256 refSlot) external view returns ( + bool success, + uint256 clBalanceGwei, + uint256 numValidators, + uint256 exitedValidators + ); +} + +contract Multiprover is LidoZKOracle, AccessControlEnumerable { + + error AdminCannotBeZero(); + + // zk Oracles commetee + error DuplicateMember(); + error NonMember(); + error QuorumTooSmall(uint256 minQuorum, uint256 receivedQuorum); + error AddressCannotBeZero(); + + error NoConsensus(); + + event MemberAdded(address indexed addr, uint256 newTotalMembers, uint256 newQuorum); + event MemberRemoved(address indexed addr, uint256 newTotalMembers, uint256 newQuorum); + event QuorumSet(uint256 newQuorum, uint256 totalMembers, uint256 prevQuorum); + + /// @notice An ACL role granting the permission to modify members list members and + /// change the quorum by calling addMember, removeMember, and setQuorum functions. + bytes32 public constant MANAGE_MEMBERS_AND_QUORUM_ROLE = + keccak256("MANAGE_MEMBERS_AND_QUORUM_ROLE"); + + /// @dev Oracle committee members' addresses array + address[] internal _memberAddresses; + + /// @dev Mapping from an oracle committee member address to the 1-based index in the + /// members array + mapping(address => uint256) internal _memberIndices1b; + + /// @dev Oracle committee members quorum value, must be larger than totalMembers // 2 + uint256 internal _quorum; + + constructor( + address admin + ) { + if (admin == address(0)) revert AdminCannotBeZero(); + + _setupRole(DEFAULT_ADMIN_ROLE, admin); + } + + function getIsMember(address addr) external view returns (bool) { + return _isMember(addr); + } + + function getMembers() external view returns ( + address[] memory addresses, + uint256[] memory lastReportedRefSlots + ) { + return _getMembers(false); + } + + function addMember(address addr, uint256 quorum) + external + onlyRole(MANAGE_MEMBERS_AND_QUORUM_ROLE) + { + _addMember(addr, quorum); + } + + function removeMember(address addr, uint256 quorum) + external + onlyRole(MANAGE_MEMBERS_AND_QUORUM_ROLE) + { + _removeMember(addr, quorum); + } + + function getQuorum() external view returns (uint256) { + return _quorum; + } + + function setQuorum(uint256 quorum) external { + // access control is performed inside the next call + _setQuorumAndCheckConsensus(quorum, _memberStates.length); + } + + /// + /// Implementation: members + /// + + function _isMember(address addr) internal view returns (bool) { + return _memberIndices1b[addr] != 0; + } + + function _getMemberIndex(address addr) internal view returns (uint256) { + uint256 index1b = _memberIndices1b[addr]; + if (index1b == 0) { + revert NonMember(); + } + unchecked { + return uint256(index1b - 1); + } + } + + function _addMember(address addr, uint256 quorum) internal { + if (_isMember(addr)) revert DuplicateMember(); + if (addr == address(0)) revert AddressCannotBeZero(); + + _memberStates.push(MemberState(0, 0)); + _memberAddresses.push(addr); + + uint256 newTotalMembers = _memberStates.length; + _memberIndices1b[addr] = newTotalMembers; + + emit MemberAdded(addr, newTotalMembers, quorum); + + _setQuorumAndCheckConsensus(quorum, newTotalMembers); + } + + function _removeMember(address addr, uint256 quorum) internal { + uint256 index = _getMemberIndex(addr); + uint256 newTotalMembers = _memberStates.length - 1; + + assert(index <= newTotalMembers); + MemberState memory memberState = _memberStates[index]; + + if (index != newTotalMembers) { + address addrToMove = _memberAddresses[newTotalMembers]; + _memberAddresses[index] = addrToMove; + _memberStates[index] = _memberStates[newTotalMembers]; + _memberIndices1b[addrToMove] = index + 1; + } + + _memberStates.pop(); + _memberAddresses.pop(); + _memberIndices1b[addr] = 0; + + emit MemberRemoved(addr, newTotalMembers, quorum); + + if (memberState.lastReportRefSlot > 0) { + // member reported at least once + ConsensusFrame memory frame = _getCurrentFrame(); + + if (memberState.lastReportRefSlot == frame.refSlot && + _getLastProcessingRefSlot() < frame.refSlot + ) { + // member reported for the current ref. slot and the consensus report + // is not processing yet => need to cancel the member's report + --_reportVariants[memberState.lastReportVariantIndex].support; + } + } + + _setQuorumAndCheckConsensus(quorum, newTotalMembers); + } + + function _getMembers(bool fastLane) internal view returns ( + address[] memory addresses, + uint256[] memory lastReportedRefSlots + ) { + uint256 totalMembers = _memberStates.length; + uint256 left; + uint256 right; + + if (fastLane) { + (left, right) = _getFastLaneSubset(_getCurrentFrame().index, totalMembers); + } else { + right = totalMembers; + } + + addresses = new address[](right - left); + lastReportedRefSlots = new uint256[](addresses.length); + + for (uint256 i = left; i < right; ++i) { + uint256 iModTotal = i % totalMembers; + MemberState memory memberState = _memberStates[iModTotal]; + uint256 k = i - left; + addresses[k] = _memberAddresses[iModTotal]; + lastReportedRefSlots[k] = memberState.lastReportRefSlot; + } + } + + function _setQuorumAndCheckConsensus(uint256 quorum, uint256 totalMembers) internal { + if (quorum <= totalMembers / 2) { + revert QuorumTooSmall(totalMembers / 2 + 1, quorum); + } + + // we're explicitly allowing quorum values greater than the number of members to + // allow effectively disabling the oracle in case something unpredictable happens + + uint256 prevQuorum = _quorum; + if (quorum != prevQuorum) { + _checkRole( + quorum == UNREACHABLE_QUORUM ? DISABLE_CONSENSUS_ROLE : MANAGE_MEMBERS_AND_QUORUM_ROLE, + _msgSender() + ); + _quorum = quorum; + emit QuorumSet(quorum, totalMembers, prevQuorum); + } + + if (_computeEpochAtTimestamp(_getTime()) >= _frameConfig.initialEpoch) { + _checkConsensus(quorum); + } + } + + + /// + /// Implementation: LidoZKOracle + /// + + function getReport(uint256 refSlot) external view override returns ( + bool success, + uint256 clBalanceGwei, + uint256 numValidators, + uint256 exitedValidators + ) { + return (true, 100, 100, 100); + } + + /// + /// Implementation: Auto-resettable fuse + /// +} \ No newline at end of file From 2eaa7d957d89b9eb5fc3df11cf261bdadd0a68d5 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 20 Mar 2024 12:03:45 +0100 Subject: [PATCH 2/6] feat: simplify members management --- contracts/0.8.9/oracle/Multiprover.sol | 120 ++++++------------------- 1 file changed, 28 insertions(+), 92 deletions(-) diff --git a/contracts/0.8.9/oracle/Multiprover.sol b/contracts/0.8.9/oracle/Multiprover.sol index 1d0296a7c..303264543 100644 --- a/contracts/0.8.9/oracle/Multiprover.sol +++ b/contracts/0.8.9/oracle/Multiprover.sol @@ -34,13 +34,12 @@ contract Multiprover is LidoZKOracle, AccessControlEnumerable { bytes32 public constant MANAGE_MEMBERS_AND_QUORUM_ROLE = keccak256("MANAGE_MEMBERS_AND_QUORUM_ROLE"); + /// @dev A quorum value that effectively disables the oracle. + uint256 internal constant UNREACHABLE_QUORUM = type(uint256).max; + /// @dev Oracle committee members' addresses array address[] internal _memberAddresses; - /// @dev Mapping from an oracle committee member address to the 1-based index in the - /// members array - mapping(address => uint256) internal _memberIndices1b; - /// @dev Oracle committee members quorum value, must be larger than totalMembers // 2 uint256 internal _quorum; @@ -52,15 +51,10 @@ contract Multiprover is LidoZKOracle, AccessControlEnumerable { _setupRole(DEFAULT_ADMIN_ROLE, admin); } - function getIsMember(address addr) external view returns (bool) { - return _isMember(addr); - } - function getMembers() external view returns ( - address[] memory addresses, - uint256[] memory lastReportedRefSlots + address[] memory addresses ) { - return _getMembers(false); + return _memberAddresses; } function addMember(address addr, uint256 quorum) @@ -83,36 +77,30 @@ contract Multiprover is LidoZKOracle, AccessControlEnumerable { function setQuorum(uint256 quorum) external { // access control is performed inside the next call - _setQuorumAndCheckConsensus(quorum, _memberStates.length); + _setQuorumAndCheckConsensus(quorum, _memberAddresses.length); } /// /// Implementation: members /// - function _isMember(address addr) internal view returns (bool) { - return _memberIndices1b[addr] != 0; - } - - function _getMemberIndex(address addr) internal view returns (uint256) { - uint256 index1b = _memberIndices1b[addr]; - if (index1b == 0) { - revert NonMember(); - } - unchecked { - return uint256(index1b - 1); + function isMember(address addr) internal view returns (bool) { + for (uint i = 0; i < _memberAddresses.length; i++) { + if (_memberAddresses[i] == addr) { + return true; + } } + return false; } + function _addMember(address addr, uint256 quorum) internal { - if (_isMember(addr)) revert DuplicateMember(); + if (isMember(addr)) revert DuplicateMember(); if (addr == address(0)) revert AddressCannotBeZero(); - _memberStates.push(MemberState(0, 0)); _memberAddresses.push(addr); - uint256 newTotalMembers = _memberStates.length; - _memberIndices1b[addr] = newTotalMembers; + uint256 newTotalMembers = _memberAddresses.length; emit MemberAdded(addr, newTotalMembers, quorum); @@ -120,91 +108,38 @@ contract Multiprover is LidoZKOracle, AccessControlEnumerable { } function _removeMember(address addr, uint256 quorum) internal { - uint256 index = _getMemberIndex(addr); - uint256 newTotalMembers = _memberStates.length - 1; - - assert(index <= newTotalMembers); - MemberState memory memberState = _memberStates[index]; - - if (index != newTotalMembers) { - address addrToMove = _memberAddresses[newTotalMembers]; - _memberAddresses[index] = addrToMove; - _memberStates[index] = _memberStates[newTotalMembers]; - _memberIndices1b[addrToMove] = index + 1; + require(isMember(addr), "Address not a member"); + + for (uint i = 0; i < _memberAddresses.length; i++) { + if (_memberAddresses[i] == addr) { + // Move the last element into the place to delete + _memberAddresses[i] = _memberAddresses[_memberAddresses.length - 1]; + // Remove the last element + _memberAddresses.pop(); + break; + } } - _memberStates.pop(); - _memberAddresses.pop(); - _memberIndices1b[addr] = 0; + uint256 newTotalMembers = _memberAddresses.length - 1; emit MemberRemoved(addr, newTotalMembers, quorum); - if (memberState.lastReportRefSlot > 0) { - // member reported at least once - ConsensusFrame memory frame = _getCurrentFrame(); - - if (memberState.lastReportRefSlot == frame.refSlot && - _getLastProcessingRefSlot() < frame.refSlot - ) { - // member reported for the current ref. slot and the consensus report - // is not processing yet => need to cancel the member's report - --_reportVariants[memberState.lastReportVariantIndex].support; - } - } - _setQuorumAndCheckConsensus(quorum, newTotalMembers); } - function _getMembers(bool fastLane) internal view returns ( - address[] memory addresses, - uint256[] memory lastReportedRefSlots - ) { - uint256 totalMembers = _memberStates.length; - uint256 left; - uint256 right; - - if (fastLane) { - (left, right) = _getFastLaneSubset(_getCurrentFrame().index, totalMembers); - } else { - right = totalMembers; - } - - addresses = new address[](right - left); - lastReportedRefSlots = new uint256[](addresses.length); - - for (uint256 i = left; i < right; ++i) { - uint256 iModTotal = i % totalMembers; - MemberState memory memberState = _memberStates[iModTotal]; - uint256 k = i - left; - addresses[k] = _memberAddresses[iModTotal]; - lastReportedRefSlots[k] = memberState.lastReportRefSlot; - } - } - function _setQuorumAndCheckConsensus(uint256 quorum, uint256 totalMembers) internal { if (quorum <= totalMembers / 2) { revert QuorumTooSmall(totalMembers / 2 + 1, quorum); } - // we're explicitly allowing quorum values greater than the number of members to - // allow effectively disabling the oracle in case something unpredictable happens - uint256 prevQuorum = _quorum; if (quorum != prevQuorum) { - _checkRole( - quorum == UNREACHABLE_QUORUM ? DISABLE_CONSENSUS_ROLE : MANAGE_MEMBERS_AND_QUORUM_ROLE, - _msgSender() - ); + _checkRole(MANAGE_MEMBERS_AND_QUORUM_ROLE, _msgSender()); _quorum = quorum; emit QuorumSet(quorum, totalMembers, prevQuorum); } - - if (_computeEpochAtTimestamp(_getTime()) >= _frameConfig.initialEpoch) { - _checkConsensus(quorum); - } } - /// /// Implementation: LidoZKOracle /// @@ -215,6 +150,7 @@ contract Multiprover is LidoZKOracle, AccessControlEnumerable { uint256 numValidators, uint256 exitedValidators ) { + refSlot; return (true, 100, 100, 100); } From 082db045305087343b80807b8e594d08df707799 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 20 Mar 2024 17:51:35 +0100 Subject: [PATCH 3/6] feat: zk interface oracle mock --- contracts/0.8.9/oracle/Multiprover.sol | 4 +-- .../test_helpers/oracle/ZkOracleMock.sol | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 contracts/0.8.9/test_helpers/oracle/ZkOracleMock.sol diff --git a/contracts/0.8.9/oracle/Multiprover.sol b/contracts/0.8.9/oracle/Multiprover.sol index 303264543..1be6678d8 100644 --- a/contracts/0.8.9/oracle/Multiprover.sol +++ b/contracts/0.8.9/oracle/Multiprover.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.9; import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol"; -interface LidoZKOracle { +interface ILidoZKOracle { function getReport(uint256 refSlot) external view returns ( bool success, uint256 clBalanceGwei, @@ -13,7 +13,7 @@ interface LidoZKOracle { ); } -contract Multiprover is LidoZKOracle, AccessControlEnumerable { +contract Multiprover is ILidoZKOracle, AccessControlEnumerable { error AdminCannotBeZero(); diff --git a/contracts/0.8.9/test_helpers/oracle/ZkOracleMock.sol b/contracts/0.8.9/test_helpers/oracle/ZkOracleMock.sol new file mode 100644 index 000000000..fb4613556 --- /dev/null +++ b/contracts/0.8.9/test_helpers/oracle/ZkOracleMock.sol @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +import { ILidoZKOracle } from "../../oracle/Multiprover.sol"; + +contract ZkOracleMock is ILidoZKOracle { + + struct Report { + bool success; + uint256 clBalanceGwei; + uint256 numValidators; + uint256 exitedValidators; + } + + mapping(uint256 => Report) public reports; + + function addReport(uint256 refSlot, Report memory report) external { + reports[refSlot] = report; + } + + function removeReport(uint256 refSlot) external { + delete reports[refSlot]; + } + + function getReport(uint256 refSlot) external view override + returns (bool success, uint256 clBalanceGwei, uint256 numValidators, uint256 exitedValidators) + { + Report memory report = reports[refSlot]; + return (report.success, report.clBalanceGwei, report.numValidators, report.exitedValidators); + } +} From a76a1aa987cf7b6e66395fae91c779ff0a7401bf Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 20 Mar 2024 17:52:01 +0100 Subject: [PATCH 4/6] feat: basic multiprover test --- test/0.8.9/oracle/multiprover.test.js | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/0.8.9/oracle/multiprover.test.js diff --git a/test/0.8.9/oracle/multiprover.test.js b/test/0.8.9/oracle/multiprover.test.js new file mode 100644 index 000000000..5b033862d --- /dev/null +++ b/test/0.8.9/oracle/multiprover.test.js @@ -0,0 +1,45 @@ +const { contract, artifacts, ethers } = require('hardhat') +const { assert } = require('../../helpers/assert') + +const { EvmSnapshot } = require('../../helpers/blockchain') + +// npx hardhat test --grep "Multiprover" +const Multiprover = artifacts.require('Multiprover') + +contract('Multiprover', ([deployer]) => { + let multiprover + let snapshot + + const log = console.log + // const log = () => {} + + before('Deploy multiprover', async function () { + multiprover = await Multiprover.new(deployer) + log('multiprover address', multiprover.address) + + snapshot = new EvmSnapshot(ethers.provider) + await snapshot.make() + }) + + afterEach(async () => { + await snapshot.rollback() + }) + + describe('Multiprover is functional', () => { + it(`have zero members`, async () => { + const members = await multiprover.getMembers() + assert.equal(members.length, 0) + log('members', members) + }) + it(`can add members`, async () => { + const role = await multiprover.MANAGE_MEMBERS_AND_QUORUM_ROLE() + log('role', role) + await multiprover.grantRole(role, deployer) + + await multiprover.addMember(deployer, 1) + + const members = await multiprover.getMembers() + log('members', members) + }) + }) +}) From 8bd19273e5526d4eaf6e1b0bbe24ab2d9406eb12 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 21 Mar 2024 15:28:35 +0100 Subject: [PATCH 5/6] fix: add SafeCast for uint256 --- contracts/0.8.9/oracle/Multiprover.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/0.8.9/oracle/Multiprover.sol b/contracts/0.8.9/oracle/Multiprover.sol index 1be6678d8..5d43ff6d3 100644 --- a/contracts/0.8.9/oracle/Multiprover.sol +++ b/contracts/0.8.9/oracle/Multiprover.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.9; import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol"; +import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; interface ILidoZKOracle { function getReport(uint256 refSlot) external view returns ( @@ -14,6 +15,7 @@ interface ILidoZKOracle { } contract Multiprover is ILidoZKOracle, AccessControlEnumerable { + using SafeCast for uint256; error AdminCannotBeZero(); From 2eb4aafffa18183e96ffd2d25794c892388570d0 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 21 Mar 2024 15:38:06 +0100 Subject: [PATCH 6/6] feat: aggregate getReport() implementation --- contracts/0.8.9/oracle/Multiprover.sol | 56 ++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/oracle/Multiprover.sol b/contracts/0.8.9/oracle/Multiprover.sol index 5d43ff6d3..a13757d0d 100644 --- a/contracts/0.8.9/oracle/Multiprover.sol +++ b/contracts/0.8.9/oracle/Multiprover.sol @@ -45,6 +45,13 @@ contract Multiprover is ILidoZKOracle, AccessControlEnumerable { /// @dev Oracle committee members quorum value, must be larger than totalMembers // 2 uint256 internal _quorum; + struct Report { + bool success; + uint256 clBalanceGwei; + uint256 numValidators; + uint256 exitedValidators; + } + constructor( address admin ) { @@ -146,17 +153,60 @@ contract Multiprover is ILidoZKOracle, AccessControlEnumerable { /// Implementation: LidoZKOracle /// + // Helper function to check if two reports are identical + function _areReportsIdentical(Report memory a, Report memory b) internal pure returns (bool) { + return a.success == b.success && + a.clBalanceGwei == b.clBalanceGwei && + a.numValidators == b.numValidators && + a.exitedValidators == b.exitedValidators; + } + + // Helper function to request a report from an oracle + function _requestReportFromOracle(ILidoZKOracle oracle, uint256 refSlot) internal view + returns (Report memory) { + (bool success, uint256 clBalanceGwei, uint256 numValidators, uint256 exitedValidators) = oracle.getReport(refSlot); + return Report(success, clBalanceGwei, numValidators, exitedValidators); + } + function getReport(uint256 refSlot) external view override returns ( bool success, uint256 clBalanceGwei, uint256 numValidators, uint256 exitedValidators ) { - refSlot; - return (true, 100, 100, 100); - } + Report[] memory reportsData = new Report[](_memberAddresses.length); + uint256[] memory reportCounts = new uint256[](_memberAddresses.length); + uint256 reports = 0; + + for (uint256 i = 0; i < _memberAddresses.length; i++) { + ILidoZKOracle oracle = ILidoZKOracle(_memberAddresses[i]); + Report memory report = _requestReportFromOracle(oracle, refSlot); + if (report.success) { + uint256 currentReportCount = 0; + for (uint256 j = 0; j < reports; j++) { + if (_areReportsIdentical(reportsData[j], report)) { + reportCounts[j]++; + currentReportCount = reportCounts[j]; + break; + } + } + if (currentReportCount == 0) { + reportsData[reports] = report; + reportCounts[reports] = 1; + currentReportCount = 1; + reports++; + } + if (currentReportCount >= _quorum) { + return (report.success, report.clBalanceGwei, report.numValidators, report.exitedValidators); + } + } + } + return (false, 0, 0, 0); + } /// /// Implementation: Auto-resettable fuse /// + + // TODO: implement the fuse } \ No newline at end of file