Skip to content

RNG Fallback #1782

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions contracts/deploy/00-home-chain-arbitration-neo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
const { ethers, deployments, getNamedAccounts, getChainId } = hre;
const { deploy, execute } = deployments;
const { ZeroAddress } = hre.ethers;
const RNG_LOOKAHEAD = 20;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
Expand Down Expand Up @@ -62,16 +61,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
const maxTotalStaked = PNK(2_000_000);
const sortitionModule = await deployUpgradable(deployments, "SortitionModuleNeo", {
from: deployer,
args: [
deployer,
klerosCoreAddress,
minStakingTime,
maxFreezingTime,
rng.address,
RNG_LOOKAHEAD,
maxStakePerJuror,
maxTotalStaked,
],
args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.address, maxStakePerJuror, maxTotalStaked],
log: true,
}); // nonce (implementation), nonce+1 (proxy)

Expand Down Expand Up @@ -107,10 +97,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)

// rng.changeSortitionModule() only if necessary
const rngContract = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG;
const currentSortitionModule = await rngContract.sortitionModule();
const currentSortitionModule = await rngContract.consumer();
if (currentSortitionModule !== sortitionModule.address) {
console.log(`rng.changeSortitionModule(${sortitionModule.address})`);
await rngContract.changeSortitionModule(sortitionModule.address);
await rngContract.changeConsumer(sortitionModule.address);
}

const core = (await hre.ethers.getContract("KlerosCoreNeo")) as KlerosCoreNeo;
Expand Down
13 changes: 8 additions & 5 deletions contracts/deploy/00-home-chain-arbitration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { DisputeKitClassic, KlerosCore, RandomizerRNG } from "../typechain-types
const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { ethers, deployments, getNamedAccounts, getChainId } = hre;
const { ZeroAddress } = hre.ethers;
const RNG_LOOKAHEAD = 20;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
Expand Down Expand Up @@ -44,7 +43,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)

const blockhashRng = await getContractOrDeploy(hre, "BlockHashRNG", {
from: deployer,
args: [],
args: [
deployer, // governor
deployer, // consumer (configured to SortitionModule later)
600, // lookaheadTime: 10 minutes in seconds
],
log: true,
});

Expand All @@ -69,7 +72,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
const maxFreezingTime = devnet ? 600 : 1800;
const sortitionModule = await deployUpgradable(deployments, "SortitionModule", {
from: deployer,
args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, RNG_LOOKAHEAD],
args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target],
log: true,
}); // nonce (implementation), nonce+1 (proxy)

Expand Down Expand Up @@ -104,10 +107,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)

// rng.changeSortitionModule() only if necessary
const rngContract = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG;
const currentSortitionModule = await rngContract.sortitionModule();
const currentSortitionModule = await rngContract.consumer();
if (currentSortitionModule !== sortitionModule.address) {
console.log(`rng.changeSortitionModule(${sortitionModule.address})`);
await rngContract.changeSortitionModule(sortitionModule.address);
await rngContract.changeConsumer(sortitionModule.address);
}

const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore;
Expand Down
10 changes: 7 additions & 3 deletions contracts/deploy/00-rng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getContractOrDeploy } from "./utils/getContractOrDeploy";
const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts, getChainId, ethers } = hre;
const { deploy } = deployments;
const RNG_LOOKAHEAD = 20;
const RNG_LOOKAHEAD_TIME = 30 * 60; // 30 minutes in seconds

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
Expand All @@ -32,11 +32,15 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)

const rng2 = await deploy("BlockHashRNG", {
from: deployer,
args: [],
args: [
deployer, // governor
sortitionModule.target, // consumer
RNG_LOOKAHEAD_TIME,
],
log: true,
});

await sortitionModule.changeRandomNumberGenerator(rng2.address, RNG_LOOKAHEAD);
await sortitionModule.changeRandomNumberGenerator(rng2.address);
};

deployArbitration.tags = ["RNG"];
Expand Down
2 changes: 0 additions & 2 deletions contracts/deploy/upgrade-sortition-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { HomeChains, isSkipped } from "./utils";

const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts, getChainId } = hre;
const RNG_LOOKAHEAD = 20;

// fallback to hardhat node signers on local network
const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address;
Expand All @@ -26,7 +25,6 @@ const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeE
1800, // minStakingTime
1800, // maxFreezingTime
rng.address,
RNG_LOOKAHEAD,
],
});
} catch (err) {
Expand Down
6 changes: 2 additions & 4 deletions contracts/src/arbitration/SortitionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,14 @@ contract SortitionModule is SortitionModuleBase, UUPSProxiable, Initializable {
/// @param _minStakingTime Minimal time to stake
/// @param _maxDrawingTime Time after which the drawing phase can be switched
/// @param _rng The random number generator.
/// @param _rngLookahead Lookahead value for rng.
function initialize(
address _governor,
KlerosCore _core,
uint256 _minStakingTime,
uint256 _maxDrawingTime,
RNG _rng,
uint256 _rngLookahead
IRNG _rng
) external reinitializer(1) {
super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead);
super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng);
}

// ************************************* //
Expand Down
27 changes: 10 additions & 17 deletions contracts/src/arbitration/SortitionModuleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pragma solidity 0.8.24;
import "./KlerosCore.sol";
import "./interfaces/ISortitionModule.sol";
import "./interfaces/IDisputeKit.sol";
import "../rng/RNG.sol";
import "../rng/IRNG.sol";
import "../libraries/Constants.sol";

/// @title SortitionModuleBase
Expand Down Expand Up @@ -63,11 +63,10 @@ abstract contract SortitionModuleBase is ISortitionModule {
uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes.
uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking.
uint256 public lastPhaseChange; // The last time the phase was changed.
uint256 public randomNumberRequestBlock; // Number of the block when RNG request was made.
uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors.
RNG public rng; // The random number generator.
IRNG public rng; // The random number generator.
uint256 public randomNumber; // Random number returned by RNG.
uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number.
// uint256 public rngLookahead; // Deprecated - WARNING: it breaks the storage layout of the contract, beta cannot be upgraded!
uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped.
uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped.
mapping(bytes32 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys.
Expand All @@ -94,16 +93,14 @@ abstract contract SortitionModuleBase is ISortitionModule {
KlerosCore _core,
uint256 _minStakingTime,
uint256 _maxDrawingTime,
RNG _rng,
uint256 _rngLookahead
IRNG _rng
) internal {
governor = _governor;
core = _core;
minStakingTime = _minStakingTime;
maxDrawingTime = _maxDrawingTime;
lastPhaseChange = block.timestamp;
rng = _rng;
rngLookahead = _rngLookahead;
delayedStakeReadIndex = 1;
}

Expand Down Expand Up @@ -137,15 +134,12 @@ abstract contract SortitionModuleBase is ISortitionModule {
maxDrawingTime = _maxDrawingTime;
}

/// @dev Changes the `_rng` and `_rngLookahead` storage variables.
/// @param _rng The new value for the `RNGenerator` storage variable.
/// @param _rngLookahead The new value for the `rngLookahead` storage variable.
function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor {
/// @dev Changes the `rng` storage variable.
/// @param _rng The new random number generator.
function changeRandomNumberGenerator(IRNG _rng) external onlyByGovernor {
rng = _rng;
rngLookahead = _rngLookahead;
if (phase == Phase.generating) {
rng.requestRandomness(block.number + rngLookahead);
randomNumberRequestBlock = block.number;
rng.requestRandomness();
}
}

Expand All @@ -160,11 +154,10 @@ abstract contract SortitionModuleBase is ISortitionModule {
"The minimum staking time has not passed yet."
);
require(disputesWithoutJurors > 0, "There are no disputes that need jurors.");
rng.requestRandomness(block.number + rngLookahead);
randomNumberRequestBlock = block.number;
rng.requestRandomness();
phase = Phase.generating;
} else if (phase == Phase.generating) {
randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead);
randomNumber = rng.receiveRandomness();
require(randomNumber != 0, "Random number is not ready yet");
phase = Phase.drawing;
} else if (phase == Phase.drawing) {
Expand Down
6 changes: 2 additions & 4 deletions contracts/src/arbitration/SortitionModuleNeo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,18 @@ contract SortitionModuleNeo is SortitionModuleBase, UUPSProxiable, Initializable
/// @param _minStakingTime Minimal time to stake
/// @param _maxDrawingTime Time after which the drawing phase can be switched
/// @param _rng The random number generator.
/// @param _rngLookahead Lookahead value for rng.
/// @param _maxStakePerJuror The maximum amount of PNK a juror can stake in a court.
/// @param _maxTotalStaked The maximum amount of PNK that can be staked in all courts.
function initialize(
address _governor,
KlerosCore _core,
uint256 _minStakingTime,
uint256 _maxDrawingTime,
RNG _rng,
uint256 _rngLookahead,
IRNG _rng,
uint256 _maxStakePerJuror,
uint256 _maxTotalStaked
) external reinitializer(2) {
super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead);
super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng);
maxStakePerJuror = _maxStakePerJuror;
maxTotalStaked = _maxTotalStaked;
}
Expand Down
122 changes: 99 additions & 23 deletions contracts/src/rng/BlockhashRNG.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,119 @@

pragma solidity 0.8.24;

import "./RNG.sol";
import "./IRNG.sol";

/// @title Random Number Generator using blockhash with fallback.
/// @author Clément Lesaege - <[email protected]>
/// @dev
/// Random Number Generator returning the blockhash with a fallback behaviour.
/// In case no one called it within the 256 blocks, it returns the previous blockhash.
/// This contract must be used when returning 0 is a worse failure mode than returning another blockhash.
/// Allows saving the random number for use in the future. It allows the contract to still access the blockhash even after 256 blocks.
contract BlockHashRNG is RNG {
mapping(uint256 block => uint256 number) public randomNumbers; // randomNumbers[block] is the random number for this block, 0 otherwise.
/// On L2 like Arbitrum block production is sporadic so block timestamp is more reliable than block number.
/// Returns 0 when no random number is available.
/// Allows saving the random number for use in the future. It allows the contract to retrieve the blockhash even after the time window.
contract BlockHashRNG is IRNG {
// ************************************* //
// * Storage * //
// ************************************* //

address public governor; // The address that can withdraw funds.
address public consumer; // The address that can request random numbers.
uint256 public immutable lookaheadTime; // Minimal time in seconds between requesting and obtaining a random number.
uint256 public requestTimestamp; // Timestamp of the current request
mapping(uint256 timestamp => uint256 number) public randomNumbers; // randomNumbers[timestamp] is the random number for this timestamp, 0 otherwise.

// ************************************* //
// * Function Modifiers * //
// ************************************* //

modifier onlyByGovernor() {
require(governor == msg.sender, "Governor only");
_;
}

modifier onlyByConsumer() {
require(consumer == msg.sender, "Consumer only");
_;
}

// ************************************* //
// * Constructor * //
// ************************************* //

/// @dev Constructor.
/// @param _governor The Governor of the contract.
/// @param _consumer The address that can request random numbers.
/// @param _lookaheadTime The time lookahead in seconds for the random number.
constructor(address _governor, address _consumer, uint256 _lookaheadTime) {
governor = _governor;
consumer = _consumer;
lookaheadTime = _lookaheadTime;
}

// ************************************* //
// * Governance * //
// ************************************* //

/// @dev Changes the governor of the contract.
/// @param _governor The new governor.
function changeGovernor(address _governor) external onlyByGovernor {
governor = _governor;
}

/// @dev Changes the consumer of the RNG.
/// @param _consumer The new consumer.
function changeConsumer(address _consumer) external onlyByGovernor {
consumer = _consumer;
}

// ************************************* //
// * State Modifiers * //
// ************************************* //

/// @dev Request a random number.
/// @param _block Block the random number is linked to.
function requestRandomness(uint256 _block) external override {
// nop
function requestRandomness() external override onlyByConsumer {
requestTimestamp = block.timestamp;
}

/// @dev Return the random number. If it has not been saved and is still computable compute it.
/// @param _block Block the random number is linked to.
/// @return randomNumber The random number or 0 if it is not ready or has not been requested.
function receiveRandomness(uint256 _block) external override returns (uint256 randomNumber) {
randomNumber = randomNumbers[_block];
function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) {
if (requestTimestamp == 0) return 0; // No request made

uint256 expectedTimestamp = requestTimestamp + lookaheadTime;

// Check if enough time has passed
if (block.timestamp < expectedTimestamp) {
return 0; // Not ready yet
}

// Check if we already have a saved random number for this timestamp window
randomNumber = randomNumbers[expectedTimestamp];
if (randomNumber != 0) {
return randomNumber;
}

if (_block < block.number) {
// The random number is not already set and can be.
if (blockhash(_block) != 0x0) {
// Normal case.
randomNumber = uint256(blockhash(_block));
} else {
// The contract was not called in time. Fallback to returning previous blockhash.
randomNumber = uint256(blockhash(block.number - 1));
}
// Use last block hash for randomness
randomNumber = uint256(blockhash(block.number - 1));
if (randomNumber != 0) {
randomNumbers[expectedTimestamp] = randomNumber;
}
randomNumbers[_block] = randomNumber;
return randomNumber;
}

// ************************************* //
// * View Functions * //
// ************************************* //

/// @dev Check if randomness is ready to be received.
/// @return ready True if randomness can be received.
function isRandomnessReady() external view returns (bool ready) {
if (requestTimestamp == 0) return false;
return block.timestamp >= requestTimestamp + lookaheadTime;
}

/// @dev Get the timestamp when randomness will be ready.
/// @return readyTimestamp The timestamp when randomness will be available.
function getRandomnessReadyTimestamp() external view returns (uint256 readyTimestamp) {
if (requestTimestamp == 0) return 0;
return requestTimestamp + lookaheadTime;
}
}
Loading