Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oracle: support "fast lane" member subset #563

Merged
merged 7 commits into from
Feb 7, 2023
31 changes: 31 additions & 0 deletions contracts/0.8.9/lib/Math.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: MIT

// See contracts/COMPILERS.md
pragma solidity 0.8.9;

library Math {
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}

function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}

/// @notice Tests if x ∈ [a, b) (mod n)
///
function pointInHalfOpenIntervalModN(uint256 x, uint256 a, uint256 b, uint256 n)
internal pure returns (bool)
{
return (x + n - a) % n < (b - a) % n;
}

/// @notice Tests if x ∈ [a, b] (mod n)
///
function pointInClosedIntervalModN(uint256 x, uint256 a, uint256 b, uint256 n)
internal pure returns (bool)
{
return (x + n - a) % n <= (b - a) % n;
}
}
292 changes: 238 additions & 54 deletions contracts/0.8.9/oracle/HashConsensus.sol

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {

function initialize(
address admin,
address pauser,
address resumer,
address consensusContract,
uint256 consensusVersion,
uint256 lastProcessingRefSlot,
Expand All @@ -120,8 +118,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
) external {
if (admin == address(0)) revert AdminCannotBeZero();
_setupRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(PAUSE_ROLE, pauser);
_grantRole(RESUME_ROLE, resumer);
_initialize(consensusContract, consensusVersion, lastProcessingRefSlot);
_setDataBoundaries(
maxExitRequestsPerReport,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ contract HashConsensusTimeTravellable is HashConsensus {
uint256 genesisTime,
uint256 epochsPerFrame,
uint256 startEpoch,
uint256 fastLaneLengthSlots,
address admin,
address reportProcessor
) HashConsensus(
Expand All @@ -23,6 +24,7 @@ contract HashConsensusTimeTravellable is HashConsensus {
genesisTime,
epochsPerFrame,
startEpoch,
fastLaneLengthSlots,
admin,
reportProcessor
) {
Expand All @@ -37,6 +39,10 @@ contract HashConsensusTimeTravellable is HashConsensus {
return _time;
}

function getTimeInSlots() external view returns (uint256) {
return _computeSlotAtTimestamp(_time);
}

function setTime(uint256 newTime) external {
_time = newTime;
}
Expand Down
5 changes: 4 additions & 1 deletion test/0.8.9/oracle/hash-consensus-deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ async function deployHashConsensus(admin, {
secondsPerSlot = SECONDS_PER_SLOT,
genesisTime = GENESIS_TIME,
epochsPerFrame = EPOCHS_PER_FRAME,
fastLaneLengthSlots = 0,
initialEpoch = 1
} = {}) {
if (!reportProcessor) {
Expand All @@ -50,6 +51,7 @@ async function deployHashConsensus(admin, {
genesisTime,
epochsPerFrame,
initialEpoch,
fastLaneLengthSlots,
admin,
reportProcessor.address,
{ from: admin }
Expand All @@ -59,7 +61,8 @@ async function deployHashConsensus(admin, {

await consensus.grantRole(await consensus.MANAGE_MEMBERS_AND_QUORUM_ROLE(), admin, { from: admin })
await consensus.grantRole(await consensus.DISABLE_CONSENSUS_ROLE(), admin, { from: admin })
await consensus.grantRole(await consensus.MANAGE_INTERVAL_ROLE(), admin, { from: admin })
await consensus.grantRole(await consensus.MANAGE_FRAME_CONFIG_ROLE(), admin, { from: admin })
await consensus.grantRole(await consensus.MANAGE_FAST_LANE_CONFIG_ROLE(), admin, { from: admin })
await consensus.grantRole(await consensus.MANAGE_REPORT_PROCESSOR_ROLE(), admin, { from: admin })

return { reportProcessor, consensus }
Expand Down
214 changes: 214 additions & 0 deletions test/0.8.9/oracle/hash-consensus-fast-lane-members.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
const { assert } = require('../../helpers/assert')
const { toNum } = require('../../helpers/utils')
const { ZERO_ADDRESS, bn } = require('@aragon/contract-helpers-test')

const {
SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, EPOCHS_PER_FRAME,
SECONDS_PER_EPOCH, SECONDS_PER_FRAME, SLOTS_PER_FRAME,
computeSlotAt, computeEpochAt, computeEpochFirstSlot, computeEpochFirstSlotAt,
computeTimestampAtSlot, computeTimestampAtEpoch,
ZERO_HASH, HASH_1, HASH_2, HASH_3, HASH_4, HASH_5,
CONSENSUS_VERSION, deployHashConsensus} = require('./hash-consensus-deploy.test')


contract('HashConsensus', ([admin, member1, member2, member3, member4, member5, stranger]) => {
context('Fast-lane members', async () => {
let consensus

const deploy = async (options = undefined) => {
const deployed = await deployHashConsensus(admin, options)
consensus = deployed.consensus
}

const setTimeToFrame0 = async () => {
await consensus.setTimeInEpochs((await consensus.getFrameConfig()).initialEpoch)
assert.equal(
+await consensus.getTimeInSlots(),
+(await consensus.getCurrentFrame()).refSlot + 1
)
}

context('State after initialization', () => {
before(deploy)

it('nobody is in the fast lane set', async () => {
assert.isFalse(await consensus.getIsFastLaneMember(member1))
assert.isFalse((await consensus.getMemberInfo(member1)).isFastLane)

assert.isFalse(await consensus.getIsFastLaneMember(member2))
assert.isFalse((await consensus.getMemberInfo(member2)).isFastLane)

assert.isFalse(await consensus.getIsFastLaneMember(member3))
assert.isFalse((await consensus.getMemberInfo(member3)).isFastLane)

assert.isEmpty((await consensus.getFastLaneMembers()).addresses)
})
})

context('Basic scenario', () => {
const fastLaneLengthSlots = 10

const frames = [
{fastLaneMembers: [member1, member2, member3], restMembers: [member4, member5]},
{fastLaneMembers: [member2, member3, member4], restMembers: [member5, member1]},
{fastLaneMembers: [member3, member4, member5], restMembers: [member1, member2]},
{fastLaneMembers: [member4, member5, member1], restMembers: [member2, member3]},
{fastLaneMembers: [member5, member1, member2], restMembers: [member3, member4]},
{fastLaneMembers: [member1, member2, member3], restMembers: [member4, member5]},
{fastLaneMembers: [member2, member3, member4], restMembers: [member5, member1]},
{fastLaneMembers: [member3, member4, member5], restMembers: [member1, member2]},
{fastLaneMembers: [member4, member5, member1], restMembers: [member2, member3]},
{fastLaneMembers: [member5, member1, member2], restMembers: [member3, member4]},
{fastLaneMembers: [member1, member2, member3], restMembers: [member4, member5]},
{fastLaneMembers: [member2, member3, member4], restMembers: [member5, member1]},
]

before(async () => {
await deploy({fastLaneLengthSlots})

await consensus.addMember(member1, 1, {from: admin})
await consensus.addMember(member2, 2, {from: admin})
await consensus.addMember(member3, 2, {from: admin})
await consensus.addMember(member4, 3, {from: admin})
await consensus.addMember(member5, 3, {from: admin})
})

before(setTimeToFrame0)

const testFrame = ({fastLaneMembers, restMembers}, index) => context(`frame ${index}`, () => {
let frame

before(async () => {
frame = await consensus.getCurrentFrame()
})

after(async () => {
await consensus.advanceTimeToNextFrameStart()
})

it(`fast lane members are calculated correctly`, async () => {
assert.isTrue(await consensus.getIsFastLaneMember(fastLaneMembers[0]))
assert.isTrue((await consensus.getMemberInfo(fastLaneMembers[0])).isFastLane)

assert.isTrue(await consensus.getIsFastLaneMember(fastLaneMembers[1]))
assert.isTrue((await consensus.getMemberInfo(fastLaneMembers[1])).isFastLane)

assert.isTrue(await consensus.getIsFastLaneMember(fastLaneMembers[2]))
assert.isTrue((await consensus.getMemberInfo(fastLaneMembers[2])).isFastLane)

assert.isFalse(await consensus.getIsFastLaneMember(restMembers[0]))
assert.isFalse((await consensus.getMemberInfo(restMembers[0])).isFastLane)

assert.isFalse(await consensus.getIsFastLaneMember(restMembers[1]))
assert.isFalse((await consensus.getMemberInfo(restMembers[1])).isFastLane)

assert.sameMembers(
(await consensus.getFastLaneMembers()).addresses,
fastLaneMembers
)

assert.sameMembers(
(await consensus.getMembers()).addresses,
[member1, member2, member3, member4, member5]
)
})

it('non-members are not in the fast lane set', async () => {
assert.isFalse(await consensus.getIsFastLaneMember(stranger))
assert.isFalse((await consensus.getMemberInfo(stranger)).isFastLane)
})

it(`fast lane members can submit a report in the first part of the frame`, async () => {
assert.isTrue((await consensus.getMemberInfo(fastLaneMembers[0])).canReport)
await consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, {from: fastLaneMembers[0]})

assert.isTrue((await consensus.getMemberInfo(fastLaneMembers[1])).canReport)
await consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, {from: fastLaneMembers[1]})

await consensus.advanceTimeBySlots(fastLaneLengthSlots - 1)

assert.isTrue((await consensus.getMemberInfo(fastLaneMembers[2])).canReport)
await consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, {from: fastLaneMembers[2]})

assert.equal((await consensus.getConsensusState()).consensusReport, HASH_1)
})

it(`non-fast lane members cannot submit a report in the first part of the frame`, async () => {
assert.isFalse((await consensus.getMemberInfo(restMembers[0])).canReport)
await assert.reverts(
consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, {from: restMembers[0]}),
'NonFastLaneMemberCannotReportWithinFastLaneInterval()'
)

assert.isFalse((await consensus.getMemberInfo(restMembers[1])).canReport)
await assert.reverts(
consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, {from: restMembers[1]}),
'NonFastLaneMemberCannotReportWithinFastLaneInterval()'
)
})

it(`non-fast lane members can submit a report during the rest of the frame`, async () => {
await consensus.advanceTimeBySlots(1)

assert.isTrue((await consensus.getMemberInfo(restMembers[0])).canReport)
await consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, {from: restMembers[0]})

assert.isTrue((await consensus.getMemberInfo(restMembers[1])).canReport)
await consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, {from: restMembers[1]})

const variants = await consensus.getReportVariants()
assert.sameOrderedMembers(variants.variants, [HASH_1])
assert.sameOrderedMembers(toNum(variants.support), [5])
})
})

frames.forEach(testFrame)
})

context('Quorum size equal to total members', () => {
before(async () => {
await deploy({fastLaneLengthSlots: 10})

await consensus.addMember(member1, 3, {from: admin})
await consensus.addMember(member2, 3, {from: admin})
await consensus.addMember(member3, 3, {from: admin})
})

before(setTimeToFrame0)

const testFrame = (frameIndex) => context(`frame ${frameIndex}`, () => {
after(async () => {
await consensus.advanceTimeToNextFrameStart()
})

it(`all members are in the fast lane set`, async () => {
assert.isTrue(await consensus.getIsFastLaneMember(member1))
assert.isTrue((await consensus.getMemberInfo(member1)).isFastLane)

assert.isTrue(await consensus.getIsFastLaneMember(member2))
assert.isTrue((await consensus.getMemberInfo(member2)).isFastLane)

assert.isTrue(await consensus.getIsFastLaneMember(member3))
assert.isTrue((await consensus.getMemberInfo(member3)).isFastLane)

assert.sameMembers(
(await consensus.getFastLaneMembers()).addresses,
[member1, member2, member3]
)

assert.sameMembers(
(await consensus.getMembers()).addresses,
[member1, member2, member3]
)
})

it('non-members are not in the fast lane set', async () => {
assert.isFalse(await consensus.getIsFastLaneMember(stranger))
assert.isFalse((await consensus.getMemberInfo(stranger)).isFastLane)
})
})

Array.from({length: 10}, (_, i) => i).forEach(testFrame)
})
})
})
14 changes: 7 additions & 7 deletions test/0.8.9/oracle/hash-consensus-frames.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ contract('HashConsensus', ([admin, member1, member2]) => {
{
assert.equal(+await consensus.getTime(), computeTimestampAtEpoch(1))

await consensus.setEpochsPerFrame(5)
await consensus.setFrameConfig(5, 0)
assert.equal(+(await consensus.getFrameConfig()).initialEpoch, 1)

/// epochs 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20
Expand All @@ -136,7 +136,7 @@ contract('HashConsensus', ([admin, member1, member2]) => {
it('increasing frame size always keeps the current start slot', async () => {
assert.equal(+await consensus.getTime(), computeTimestampAtEpoch(1))

await consensus.setEpochsPerFrame(5)
await consensus.setFrameConfig(5, 0)
assert.equal(+(await consensus.getFrameConfig()).initialEpoch, 1)

/// we're at the last slot of the frame 1 spanning epochs 6-10
Expand All @@ -153,7 +153,7 @@ contract('HashConsensus', ([admin, member1, member2]) => {
assert.equal(+frame.refSlot, computeEpochFirstSlot(6) - 1)
assert.equal(+frame.reportProcessingDeadlineSlot, computeEpochFirstSlot(11) - 1)

await consensus.setEpochsPerFrame(7)
await consensus.setFrameConfig(7, 0)

const newFrame = await consensus.getCurrentFrame()
assert.equal(+newFrame.refSlot, computeEpochFirstSlot(6) - 1)
Expand All @@ -163,7 +163,7 @@ contract('HashConsensus', ([admin, member1, member2]) => {
it(`decreasing the frame size cannot decrease the current reference slot`, async () => {
assert.equal(+await consensus.getTime(), computeTimestampAtEpoch(1))

await consensus.setEpochsPerFrame(5)
await consensus.setFrameConfig(5, 0)
assert.equal(+(await consensus.getFrameConfig()).initialEpoch, 1)

/// we're in the first half of the frame 1 spanning epochs 6-10
Expand All @@ -180,7 +180,7 @@ contract('HashConsensus', ([admin, member1, member2]) => {
assert.equal(+frame.refSlot, computeEpochFirstSlot(6) - 1)
assert.equal(+frame.reportProcessingDeadlineSlot, computeEpochFirstSlot(11) - 1)

await consensus.setEpochsPerFrame(4)
await consensus.setFrameConfig(4, 0)

const newFrame = await consensus.getCurrentFrame()
assert.equal(+newFrame.refSlot, computeEpochFirstSlot(6) - 1)
Expand All @@ -192,7 +192,7 @@ contract('HashConsensus', ([admin, member1, member2]) => {
{
assert.equal(+await consensus.getTime(), computeTimestampAtEpoch(1))

await consensus.setEpochsPerFrame(5)
await consensus.setFrameConfig(5, 0)
assert.equal(+(await consensus.getFrameConfig()).initialEpoch, 1)

/// we're at the end of the frame 1 spanning epochs 6-10
Expand All @@ -209,7 +209,7 @@ contract('HashConsensus', ([admin, member1, member2]) => {
assert.equal(+frame.refSlot, computeEpochFirstSlot(6) - 1)
assert.equal(+frame.reportProcessingDeadlineSlot, computeEpochFirstSlot(11) - 1)

await consensus.setEpochsPerFrame(4)
await consensus.setFrameConfig(4, 0)

const newFrame = await consensus.getCurrentFrame()
assert.equal(+newFrame.refSlot, computeEpochFirstSlot(10) - 1)
Expand Down
Loading