@@ -4,6 +4,7 @@ pragma solidity 0.8.9;
4
4
5
5
import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol " ;
6
6
7
+ import { Math } from "../lib/Math.sol " ;
7
8
import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol " ;
8
9
9
10
@@ -66,10 +67,12 @@ contract HashConsensus is AccessControlEnumerable {
66
67
error DuplicateReport ();
67
68
error EmptyReport ();
68
69
error StaleReport ();
70
+ error NonFastLaneMemberCannotReportWithinFastLaneInterval ();
69
71
error NewProcessorCannotBeTheSame ();
70
72
error ConsensusReportAlreadyProcessing ();
71
73
72
74
event FrameConfigSet (uint256 newInitialEpoch , uint256 newEpochsPerFrame );
75
+ event FastLaneConfigSet (uint256 fastLaneLengthSlots );
73
76
event MemberAdded (address indexed addr , uint256 newTotalMembers , uint256 newQuorum );
74
77
event MemberRemoved (address indexed addr , uint256 newTotalMembers , uint256 newQuorum );
75
78
event QuorumSet (uint256 newQuorum , uint256 totalMembers , uint256 prevQuorum );
@@ -80,15 +83,18 @@ contract HashConsensus is AccessControlEnumerable {
80
83
struct FrameConfig {
81
84
uint64 initialEpoch;
82
85
uint64 epochsPerFrame;
86
+ uint64 fastLaneLengthSlots;
83
87
}
84
88
85
89
/// @dev Oracle reporting is divided into frames, each lasting the same number of slots
86
90
struct ConsensusFrame {
91
+ // frame index; increments by 1 with each frame but resets to zero on frame size change
92
+ uint256 index;
87
93
// the slot at which to read the state around which consensus is being reached;
88
94
// if the slot contains a block, the state should include all changes from that block
89
- uint64 refSlot;
95
+ uint256 refSlot;
90
96
// the last slot at which a report can be processed
91
- uint64 reportProcessingDeadlineSlot;
97
+ uint256 reportProcessingDeadlineSlot;
92
98
}
93
99
94
100
struct ReportingState {
@@ -126,7 +132,11 @@ contract HashConsensus is AccessControlEnumerable {
126
132
127
133
/// @notice An ACL role granting the permission to change reporting interval
128
134
/// 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 " );
130
140
131
141
/// @notice An ACL role granting the permission to change еру report processor
132
142
/// contract by calling setReportProcessor.
@@ -177,6 +187,7 @@ contract HashConsensus is AccessControlEnumerable {
177
187
uint256 genesisTime ,
178
188
uint256 epochsPerFrame ,
179
189
uint256 initialEpoch ,
190
+ uint256 fastLaneLengthSlots ,
180
191
address admin ,
181
192
address reportProcessor
182
193
) {
@@ -187,6 +198,7 @@ contract HashConsensus is AccessControlEnumerable {
187
198
if (admin == address (0 )) revert AdminCannotBeZero ();
188
199
_setupRole (DEFAULT_ADMIN_ROLE, admin);
189
200
_setFrameConfig (initialEpoch, epochsPerFrame);
201
+ _setFastLaneConfig (fastLaneLengthSlots);
190
202
// zero address is allowed here, meaning "no processor"
191
203
_reportProcessor = reportProcessor;
192
204
}
@@ -222,7 +234,9 @@ contract HashConsensus is AccessControlEnumerable {
222
234
return (frame.refSlot, frame.reportProcessingDeadlineSlot);
223
235
}
224
236
225
- function setEpochsPerFrame (uint256 epochsPerFrame ) external onlyRole (MANAGE_INTERVAL_ROLE) {
237
+ function setEpochsPerFrame (uint256 epochsPerFrame )
238
+ external onlyRole (MANAGE_FRAME_CONFIG_ROLE)
239
+ {
226
240
// Updates epochsPerFrame in a way that either keeps the current reference slot the same
227
241
// or increases it by at least the minimum of old and new frame sizes.
228
242
uint256 timestamp = _getTime ();
@@ -238,26 +252,53 @@ contract HashConsensus is AccessControlEnumerable {
238
252
return _isMember (addr);
239
253
}
240
254
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
+
241
272
function getMembers () external view returns (
242
273
address [] memory addresses ,
243
274
uint256 [] memory lastReportedRefSlots
244
275
) {
245
- addresses = new address [](_members. length );
246
- lastReportedRefSlots = new uint256 [](addresses. length );
276
+ return _getMembers ( false );
277
+ }
247
278
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 );
253
287
}
254
288
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.
256
291
///
257
292
/// @param addr The member address.
258
293
///
259
294
/// @return isMember Whether the provided address is a member of the oracle.
260
295
///
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
+ ///
261
302
/// @return lastReportRefSlot The last reference slot for which the member reported a data hash.
262
303
///
263
304
/// @return currentRefSlot Current reference slot.
@@ -268,6 +309,8 @@ contract HashConsensus is AccessControlEnumerable {
268
309
///
269
310
function getMemberInfo (address addr ) external view returns (
270
311
bool isMember ,
312
+ bool isFastLane ,
313
+ bool canReport ,
271
314
uint256 lastReportRefSlot ,
272
315
uint256 currentRefSlot ,
273
316
bytes32 memberReportForCurrentRefSlot
@@ -285,9 +328,37 @@ contract HashConsensus is AccessControlEnumerable {
285
328
memberReportForCurrentRefSlot = lastReportRefSlot == frame.refSlot
286
329
? _reportVariants[member.lastReportVariantIndex].hash
287
330
: 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
+ }
288
338
}
289
339
}
290
340
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
+
291
362
function addMember (address addr , uint256 quorum )
292
363
external
293
364
onlyRole (MANAGE_MEMBERS_AND_QUORUM_ROLE)
@@ -396,36 +467,57 @@ contract HashConsensus is AccessControlEnumerable {
396
467
397
468
function _setFrameConfig (uint256 initialEpoch , uint256 epochsPerFrame ) internal {
398
469
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
+ );
400
475
emit FrameConfigSet (initialEpoch, epochsPerFrame);
401
476
}
402
477
403
478
function _getCurrentFrame () internal view returns (ConsensusFrame memory ) {
404
- return _getFrameAtTimestamp ( _getTime () );
479
+ return _getCurrentFrame (_frameConfig );
405
480
}
406
481
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
+ }
409
485
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);
411
491
uint256 frameStartSlot = _computeStartSlotAtEpoch (frameStartEpoch);
412
492
uint256 nextFrameStartSlot = frameStartSlot + config.epochsPerFrame * SLOTS_PER_EPOCH;
413
493
414
494
return ConsensusFrame ({
495
+ index: frameIndex,
415
496
refSlot: uint64 (frameStartSlot - 1 ),
416
497
reportProcessingDeadlineSlot: uint64 (nextFrameStartSlot - 1 )
417
498
});
418
499
}
419
500
420
501
function _computeFrameStartEpoch (uint256 timestamp , FrameConfig memory config )
421
502
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 )
422
515
{
423
516
uint256 epoch = _computeEpochAtTimestamp (timestamp);
424
517
if (epoch < config.initialEpoch) {
425
518
revert InitialEpochIsYetToArrive ();
426
519
}
427
- uint256 frameIndex = (epoch - config.initialEpoch) / config.epochsPerFrame;
428
- return config.initialEpoch + frameIndex * config.epochsPerFrame;
520
+ return (epoch - config.initialEpoch) / config.epochsPerFrame;
429
521
}
430
522
431
523
function _computeTimestampAtSlot (uint256 slot ) internal view returns (uint256 ) {
@@ -518,6 +610,56 @@ contract HashConsensus is AccessControlEnumerable {
518
610
_setQuorumAndCheckConsensus (quorum, newTotalMembers);
519
611
}
520
612
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
+
521
663
///
522
664
/// Implementation: consensus
523
665
///
@@ -535,12 +677,19 @@ contract HashConsensus is AccessControlEnumerable {
535
677
536
678
uint256 timestamp = _getTime ();
537
679
uint256 currentSlot = _computeSlotAtTimestamp (timestamp);
538
- ConsensusFrame memory frame = _getFrameAtTimestamp (timestamp);
680
+ FrameConfig memory config = _frameConfig;
681
+ ConsensusFrame memory frame = _getFrameAtTimestamp (timestamp, config);
539
682
540
683
if (report == ZERO_HASH) revert EmptyReport ();
541
684
if (slot != frame.refSlot) revert InvalidSlot ();
542
685
if (currentSlot > frame.reportProcessingDeadlineSlot) revert StaleReport ();
543
686
687
+ if (currentSlot <= frame.refSlot + config.fastLaneLengthSlots &&
688
+ ! _isFastLaneMember (memberIndex, frame.index)
689
+ ) {
690
+ revert NonFastLaneMemberCannotReportWithinFastLaneInterval ();
691
+ }
692
+
544
693
if (slot <= _getLastProcessingRefSlot ()) {
545
694
// consensus for the ref. slot was already reached and consensus report is processing
546
695
if (slot == member.lastReportRefSlot) {
@@ -606,7 +755,7 @@ contract HashConsensus is AccessControlEnumerable {
606
755
if (_reportingState.lastConsensusRefSlot != frame.refSlot ||
607
756
_reportingState.lastConsensusVariantIndex != variantIndex
608
757
) {
609
- _reportingState.lastConsensusRefSlot = frame.refSlot;
758
+ _reportingState.lastConsensusRefSlot = uint64 ( frame.refSlot) ;
610
759
_reportingState.lastConsensusVariantIndex = uint64 (variantIndex);
611
760
612
761
_submitReportForProcessing (frame, report);
@@ -641,7 +790,7 @@ contract HashConsensus is AccessControlEnumerable {
641
790
642
791
function _checkConsensus (uint256 quorum ) internal {
643
792
uint256 timestamp = _getTime ();
644
- ConsensusFrame memory frame = _getFrameAtTimestamp (timestamp);
793
+ ConsensusFrame memory frame = _getFrameAtTimestamp (timestamp, _frameConfig );
645
794
646
795
if (_computeSlotAtTimestamp (timestamp) > frame.reportProcessingDeadlineSlot) {
647
796
// reference slot is not reportable anymore
@@ -661,7 +810,7 @@ contract HashConsensus is AccessControlEnumerable {
661
810
}
662
811
}
663
812
664
- function _getConsensusReport (uint64 currentRefSlot , uint256 quorum )
813
+ function _getConsensusReport (uint256 currentRefSlot , uint256 quorum )
665
814
internal view returns (bytes32 report , int256 variantIndex , uint256 support )
666
815
{
667
816
if (_reportingState.lastReportRefSlot != currentRefSlot) {
0 commit comments