Skip to content

Commit a5a9bad

Browse files
committed
hash consensus: introduce fast lane member set
1 parent bf1a4ad commit a5a9bad

File tree

7 files changed

+353
-29
lines changed

7 files changed

+353
-29
lines changed

contracts/0.8.9/lib/Math.sol

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
2+
// SPDX-License-Identifier: MIT
3+
4+
// See contracts/COMPILERS.md
5+
pragma solidity 0.8.9;
6+
7+
library Math {
8+
function max(uint256 a, uint256 b) internal pure returns (uint256) {
9+
return a > b ? a : b;
10+
}
11+
12+
function min(uint256 a, uint256 b) internal pure returns (uint256) {
13+
return a < b ? a : b;
14+
}
15+
16+
/// @notice Tests if x ∈ [a, b) (mod n)
17+
///
18+
function pointInHalfOpenIntervalModN(uint256 x, uint256 a, uint256 b, uint256 n)
19+
internal pure returns (bool)
20+
{
21+
return (x + n - a) % n < (b - a) % n;
22+
}
23+
}

contracts/0.8.9/oracle/HashConsensus.sol

Lines changed: 172 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity 0.8.9;
44

55
import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol";
66

7+
import { Math } from "../lib/Math.sol";
78
import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol";
89

910

@@ -66,10 +67,12 @@ contract HashConsensus is AccessControlEnumerable {
6667
error DuplicateReport();
6768
error EmptyReport();
6869
error StaleReport();
70+
error NonFastLaneMemberCannotReportWithinFastLaneInterval();
6971
error NewProcessorCannotBeTheSame();
7072
error ConsensusReportAlreadyProcessing();
7173

7274
event FrameConfigSet(uint256 newInitialEpoch, uint256 newEpochsPerFrame);
75+
event FastLaneConfigSet(uint256 fastLaneLengthSlots);
7376
event MemberAdded(address indexed addr, uint256 newTotalMembers, uint256 newQuorum);
7477
event MemberRemoved(address indexed addr, uint256 newTotalMembers, uint256 newQuorum);
7578
event QuorumSet(uint256 newQuorum, uint256 totalMembers, uint256 prevQuorum);
@@ -80,15 +83,18 @@ contract HashConsensus is AccessControlEnumerable {
8083
struct FrameConfig {
8184
uint64 initialEpoch;
8285
uint64 epochsPerFrame;
86+
uint64 fastLaneLengthSlots;
8387
}
8488

8589
/// @dev Oracle reporting is divided into frames, each lasting the same number of slots
8690
struct ConsensusFrame {
91+
// frame index; increments by 1 with each frame but resets to zero on frame size change
92+
uint256 index;
8793
// the slot at which to read the state around which consensus is being reached;
8894
// if the slot contains a block, the state should include all changes from that block
89-
uint64 refSlot;
95+
uint256 refSlot;
9096
// the last slot at which a report can be processed
91-
uint64 reportProcessingDeadlineSlot;
97+
uint256 reportProcessingDeadlineSlot;
9298
}
9399

94100
struct ReportingState {
@@ -126,7 +132,11 @@ contract HashConsensus is AccessControlEnumerable {
126132

127133
/// @notice An ACL role granting the permission to change reporting interval
128134
/// duration by calling setEpochsPerFrame.
129-
bytes32 public constant MANAGE_INTERVAL_ROLE = keccak256("MANAGE_INTERVAL_ROLE");
135+
bytes32 public constant MANAGE_FRAME_CONFIG_ROLE = keccak256("MANAGE_FRAME_CONFIG_ROLE");
136+
137+
/// @notice An ACL role granting the permission to change fast lane reporting interval
138+
/// length by calling setFastLaneLengthSlots.
139+
bytes32 public constant MANAGE_FAST_LANE_CONFIG_ROLE = keccak256("MANAGE_FAST_LANE_CONFIG_ROLE");
130140

131141
/// @notice An ACL role granting the permission to change еру report processor
132142
/// contract by calling setReportProcessor.
@@ -177,6 +187,7 @@ contract HashConsensus is AccessControlEnumerable {
177187
uint256 genesisTime,
178188
uint256 epochsPerFrame,
179189
uint256 initialEpoch,
190+
uint256 fastLaneLengthSlots,
180191
address admin,
181192
address reportProcessor
182193
) {
@@ -187,6 +198,7 @@ contract HashConsensus is AccessControlEnumerable {
187198
if (admin == address(0)) revert AdminCannotBeZero();
188199
_setupRole(DEFAULT_ADMIN_ROLE, admin);
189200
_setFrameConfig(initialEpoch, epochsPerFrame);
201+
_setFastLaneConfig(fastLaneLengthSlots);
190202
// zero address is allowed here, meaning "no processor"
191203
_reportProcessor = reportProcessor;
192204
}
@@ -222,7 +234,9 @@ contract HashConsensus is AccessControlEnumerable {
222234
return (frame.refSlot, frame.reportProcessingDeadlineSlot);
223235
}
224236

225-
function setEpochsPerFrame(uint256 epochsPerFrame) external onlyRole(MANAGE_INTERVAL_ROLE) {
237+
function setEpochsPerFrame(uint256 epochsPerFrame)
238+
external onlyRole(MANAGE_FRAME_CONFIG_ROLE)
239+
{
226240
// Updates epochsPerFrame in a way that either keeps the current reference slot the same
227241
// or increases it by at least the minimum of old and new frame sizes.
228242
uint256 timestamp = _getTime();
@@ -238,26 +252,53 @@ contract HashConsensus is AccessControlEnumerable {
238252
return _isMember(addr);
239253
}
240254

255+
/// @notice Returns whether the given address is a fast lane member for the current reporting
256+
/// frame.
257+
///
258+
/// Fast lane members can, and expected to, submit a report during the first part of the frame
259+
/// defined via `setFastLaneConfig`. Non-fast-lane members are only allowed to submit a report
260+
/// after the "fast-lane" part of the frame passes.
261+
///
262+
/// This is done to encourage each oracle from the full set to participate in reporting on a
263+
/// regular basis, and identify any malfunctioning members.
264+
///
265+
function getIsFastLaneMember(address addr) external view returns (bool) {
266+
uint256 index1b = _memberIndices1b[addr];
267+
unchecked {
268+
return index1b > 0 && _isFastLaneMember(index1b - 1, _getCurrentFrame().index);
269+
}
270+
}
271+
241272
function getMembers() external view returns (
242273
address[] memory addresses,
243274
uint256[] memory lastReportedRefSlots
244275
) {
245-
addresses = new address[](_members.length);
246-
lastReportedRefSlots = new uint256[](addresses.length);
276+
return _getMembers(false);
277+
}
247278

248-
for (uint256 i = 0; i < addresses.length; ++i) {
249-
MemberState storage member = _members[i];
250-
addresses[i] = member.addr;
251-
lastReportedRefSlots[i] = member.lastReportRefSlot;
252-
}
279+
/// @notice Returns the subset of oracle committee members (consisting of `quorum` items) that
280+
/// changes on each frame. See `getIsFastLaneMember`.
281+
///
282+
function getFastLaneMembers() external view returns (
283+
address[] memory addresses,
284+
uint256[] memory lastReportedRefSlots
285+
) {
286+
return _getMembers(true);
253287
}
254288

255-
/// @notice Returns the information related to an oracle committee member with the given address.
289+
/// @notice Returns the extended information related to an oracle committee member with the
290+
/// given address.
256291
///
257292
/// @param addr The member address.
258293
///
259294
/// @return isMember Whether the provided address is a member of the oracle.
260295
///
296+
/// @return isFastLane Whether the oracle member is in the fast lane members subset of the
297+
/// current reporting frame. See `getIsFastLaneMember`.
298+
///
299+
/// @return canReport Whether the oracle member is allowed to submit a report at the moment
300+
/// of the call.
301+
///
261302
/// @return lastReportRefSlot The last reference slot for which the member reported a data hash.
262303
///
263304
/// @return currentRefSlot Current reference slot.
@@ -268,6 +309,8 @@ contract HashConsensus is AccessControlEnumerable {
268309
///
269310
function getMemberInfo(address addr) external view returns (
270311
bool isMember,
312+
bool isFastLane,
313+
bool canReport,
271314
uint256 lastReportRefSlot,
272315
uint256 currentRefSlot,
273316
bytes32 memberReportForCurrentRefSlot
@@ -285,9 +328,37 @@ contract HashConsensus is AccessControlEnumerable {
285328
memberReportForCurrentRefSlot = lastReportRefSlot == frame.refSlot
286329
? _reportVariants[member.lastReportVariantIndex].hash
287330
: ZERO_HASH;
331+
uint256 slot = _computeSlotAtTimestamp(_getTime());
332+
canReport = slot <= frame.reportProcessingDeadlineSlot &&
333+
frame.refSlot > _getLastProcessingRefSlot();
334+
isFastLane = _isFastLaneMember(index, frame.index);
335+
if (!isFastLane && canReport) {
336+
canReport = slot > frame.refSlot + _frameConfig.fastLaneLengthSlots;
337+
}
288338
}
289339
}
290340

341+
/// @notice Sets the duration of the interval starting at the beginning of the frame during
342+
/// which only the selected "fast lane" subset of oracle committee memebrs can (and expected
343+
/// to) submit a report.
344+
///
345+
/// The fast lane subset is a subset consisting of `quorum` oracles that changes on each frame.
346+
/// This is done to encourage each oracle from the full set to participate in reporting on a
347+
/// regular basis, and identify any malfunctioning members.
348+
///
349+
/// The subset selection is implemented as a sliding window of the `quorum` width over member
350+
/// indices (mod total members). The window advances by one index each reporting frame.
351+
///
352+
/// @param fastLaneLengthSlots The length of the fast lane reporting interval in slots. Setting
353+
/// it to zero disables the fast lane subset, alloing any oracle to report starting from
354+
/// the first slot of a frame and until the frame's reporting deadline.
355+
///
356+
function setFastLaneLengthSlots(uint256 fastLaneLengthSlots)
357+
external onlyRole(MANAGE_FAST_LANE_CONFIG_ROLE)
358+
{
359+
_setFastLaneConfig(fastLaneLengthSlots);
360+
}
361+
291362
function addMember(address addr, uint256 quorum)
292363
external
293364
onlyRole(MANAGE_MEMBERS_AND_QUORUM_ROLE)
@@ -396,36 +467,57 @@ contract HashConsensus is AccessControlEnumerable {
396467

397468
function _setFrameConfig(uint256 initialEpoch, uint256 epochsPerFrame) internal {
398469
if (epochsPerFrame == 0) revert EpochsPerFrameCannotBeZero();
399-
_frameConfig = FrameConfig(initialEpoch.toUint64(), epochsPerFrame.toUint64());
470+
_frameConfig = FrameConfig(
471+
initialEpoch.toUint64(),
472+
epochsPerFrame.toUint64(),
473+
_frameConfig.fastLaneLengthSlots
474+
);
400475
emit FrameConfigSet(initialEpoch, epochsPerFrame);
401476
}
402477

403478
function _getCurrentFrame() internal view returns (ConsensusFrame memory) {
404-
return _getFrameAtTimestamp(_getTime());
479+
return _getCurrentFrame(_frameConfig);
405480
}
406481

407-
function _getFrameAtTimestamp(uint256 timestamp) internal view returns (ConsensusFrame memory) {
408-
FrameConfig memory config = _frameConfig;
482+
function _getCurrentFrame(FrameConfig memory config) internal view returns (ConsensusFrame memory) {
483+
return _getFrameAtTimestamp(_getTime(), config);
484+
}
409485

410-
uint256 frameStartEpoch = _computeFrameStartEpoch(timestamp, config);
486+
function _getFrameAtTimestamp(uint256 timestamp, FrameConfig memory config)
487+
internal view returns (ConsensusFrame memory)
488+
{
489+
uint256 frameIndex = _computeFrameIndex(timestamp, config);
490+
uint256 frameStartEpoch = _computeStartEpochOfFrameWithIndex(frameIndex, config);
411491
uint256 frameStartSlot = _computeStartSlotAtEpoch(frameStartEpoch);
412492
uint256 nextFrameStartSlot = frameStartSlot + config.epochsPerFrame * SLOTS_PER_EPOCH;
413493

414494
return ConsensusFrame({
495+
index: frameIndex,
415496
refSlot: uint64(frameStartSlot - 1),
416497
reportProcessingDeadlineSlot: uint64(nextFrameStartSlot - 1)
417498
});
418499
}
419500

420501
function _computeFrameStartEpoch(uint256 timestamp, FrameConfig memory config)
421502
internal view returns (uint256)
503+
{
504+
return _computeStartEpochOfFrameWithIndex(_computeFrameIndex(timestamp, config), config);
505+
}
506+
507+
function _computeStartEpochOfFrameWithIndex(uint256 frameIndex, FrameConfig memory config)
508+
internal pure returns (uint256)
509+
{
510+
return config.initialEpoch + frameIndex * config.epochsPerFrame;
511+
}
512+
513+
function _computeFrameIndex(uint256 timestamp, FrameConfig memory config)
514+
internal view returns (uint256)
422515
{
423516
uint256 epoch = _computeEpochAtTimestamp(timestamp);
424517
if (epoch < config.initialEpoch) {
425518
revert InitialEpochIsYetToArrive();
426519
}
427-
uint256 frameIndex = (epoch - config.initialEpoch) / config.epochsPerFrame;
428-
return config.initialEpoch + frameIndex * config.epochsPerFrame;
520+
return (epoch - config.initialEpoch) / config.epochsPerFrame;
429521
}
430522

431523
function _computeTimestampAtSlot(uint256 slot) internal view returns (uint256) {
@@ -518,6 +610,56 @@ contract HashConsensus is AccessControlEnumerable {
518610
_setQuorumAndCheckConsensus(quorum, newTotalMembers);
519611
}
520612

613+
function _setFastLaneConfig(uint256 fastLaneLengthSlots) internal {
614+
if (fastLaneLengthSlots != _frameConfig.fastLaneLengthSlots) {
615+
_frameConfig.fastLaneLengthSlots = fastLaneLengthSlots.toUint64();
616+
emit FastLaneConfigSet(fastLaneLengthSlots);
617+
}
618+
}
619+
620+
/// @dev Returns start and past-end incides (mod totalMembers) of the fast lane members subset.
621+
///
622+
function _getFastLaneSubset(uint256 frameIndex, uint256 totalMembers)
623+
internal view returns (uint256 startIndex, uint256 pastEndIndex)
624+
{
625+
startIndex = frameIndex % totalMembers;
626+
pastEndIndex = startIndex + _quorum;
627+
}
628+
629+
/// @dev Tests whether the member with the given `index` is in the fast lane subset for the
630+
/// given reporting `frameIndex`.
631+
///
632+
function _isFastLaneMember(uint256 index, uint256 frameIndex) internal view returns (bool) {
633+
uint256 totalMembers = _members.length;
634+
(uint256 flLeft, uint256 flPastRight) = _getFastLaneSubset(frameIndex, totalMembers);
635+
return Math.pointInHalfOpenIntervalModN(index, flLeft, flPastRight, totalMembers);
636+
}
637+
638+
function _getMembers(bool fastLane) internal view returns (
639+
address[] memory addresses,
640+
uint256[] memory lastReportedRefSlots
641+
) {
642+
uint256 totalMembers = _members.length;
643+
uint256 left;
644+
uint256 right;
645+
646+
if (fastLane) {
647+
(left, right) = _getFastLaneSubset(_getCurrentFrame().index, totalMembers);
648+
} else {
649+
right = totalMembers;
650+
}
651+
652+
addresses = new address[](right - left);
653+
lastReportedRefSlots = new uint256[](addresses.length);
654+
655+
for (uint256 i = left; i < right; ++i) {
656+
MemberState storage member = _members[i % totalMembers];
657+
uint256 k = i - left;
658+
addresses[k] = member.addr;
659+
lastReportedRefSlots[k] = member.lastReportRefSlot;
660+
}
661+
}
662+
521663
///
522664
/// Implementation: consensus
523665
///
@@ -535,12 +677,19 @@ contract HashConsensus is AccessControlEnumerable {
535677

536678
uint256 timestamp = _getTime();
537679
uint256 currentSlot = _computeSlotAtTimestamp(timestamp);
538-
ConsensusFrame memory frame = _getFrameAtTimestamp(timestamp);
680+
FrameConfig memory config = _frameConfig;
681+
ConsensusFrame memory frame = _getFrameAtTimestamp(timestamp, config);
539682

540683
if (report == ZERO_HASH) revert EmptyReport();
541684
if (slot != frame.refSlot) revert InvalidSlot();
542685
if (currentSlot > frame.reportProcessingDeadlineSlot) revert StaleReport();
543686

687+
if (currentSlot <= frame.refSlot + config.fastLaneLengthSlots &&
688+
!_isFastLaneMember(memberIndex, frame.index)
689+
) {
690+
revert NonFastLaneMemberCannotReportWithinFastLaneInterval();
691+
}
692+
544693
if (slot <= _getLastProcessingRefSlot()) {
545694
// consensus for the ref. slot was already reached and consensus report is processing
546695
if (slot == member.lastReportRefSlot) {
@@ -606,7 +755,7 @@ contract HashConsensus is AccessControlEnumerable {
606755
if (_reportingState.lastConsensusRefSlot != frame.refSlot ||
607756
_reportingState.lastConsensusVariantIndex != variantIndex
608757
) {
609-
_reportingState.lastConsensusRefSlot = frame.refSlot;
758+
_reportingState.lastConsensusRefSlot = uint64(frame.refSlot);
610759
_reportingState.lastConsensusVariantIndex = uint64(variantIndex);
611760

612761
_submitReportForProcessing(frame, report);
@@ -641,7 +790,7 @@ contract HashConsensus is AccessControlEnumerable {
641790

642791
function _checkConsensus(uint256 quorum) internal {
643792
uint256 timestamp = _getTime();
644-
ConsensusFrame memory frame = _getFrameAtTimestamp(timestamp);
793+
ConsensusFrame memory frame = _getFrameAtTimestamp(timestamp, _frameConfig);
645794

646795
if (_computeSlotAtTimestamp(timestamp) > frame.reportProcessingDeadlineSlot) {
647796
// reference slot is not reportable anymore
@@ -661,7 +810,7 @@ contract HashConsensus is AccessControlEnumerable {
661810
}
662811
}
663812

664-
function _getConsensusReport(uint64 currentRefSlot, uint256 quorum)
813+
function _getConsensusReport(uint256 currentRefSlot, uint256 quorum)
665814
internal view returns (bytes32 report, int256 variantIndex, uint256 support)
666815
{
667816
if (_reportingState.lastReportRefSlot != currentRefSlot) {

0 commit comments

Comments
 (0)