Skip to content

Commit f0746bd

Browse files
committed
fix: various fixes for BlockhashRNG and RNGWillFallback, use block timestamp
1 parent ec3cd79 commit f0746bd

File tree

5 files changed

+188
-59
lines changed

5 files changed

+188
-59
lines changed

contracts/deploy/00-home-chain-arbitration.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
4343

4444
const blockhashRng = await getContractOrDeploy(hre, "BlockHashRNG", {
4545
from: deployer,
46-
args: [],
46+
args: [
47+
deployer, // governor
48+
deployer, // consumer (configured to SortitionModule later)
49+
600, // lookaheadTime: 10 minutes in seconds
50+
],
4751
log: true,
4852
});
4953

contracts/deploy/00-rng.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getContractOrDeploy } from "./utils/getContractOrDeploy";
88
const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
99
const { deployments, getNamedAccounts, getChainId, ethers } = hre;
1010
const { deploy } = deployments;
11-
const RNG_LOOKAHEAD = 20;
11+
const RNG_LOOKAHEAD_TIME = 30 * 60; // 30 minutes in seconds
1212

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

3333
const rng2 = await deploy("BlockHashRNG", {
3434
from: deployer,
35-
args: [],
35+
args: [
36+
deployer, // governor
37+
sortitionModule.target, // consumer
38+
RNG_LOOKAHEAD_TIME,
39+
],
3640
log: true,
3741
});
3842

39-
await sortitionModule.changeRandomNumberGenerator(rng2.address, RNG_LOOKAHEAD);
43+
await sortitionModule.changeRandomNumberGenerator(rng2.address);
4044
};
4145

4246
deployArbitration.tags = ["RNG"];

contracts/src/rng/BlockhashRNG.sol

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,114 @@ import "./IRNG.sol";
77
/// @title Random Number Generator using blockhash with fallback.
88
/// @dev
99
/// Random Number Generator returning the blockhash with a fallback behaviour.
10-
/// In case no one called it within the 256 blocks, it returns the previous blockhash.
11-
/// This contract must be used when returning 0 is a worse failure mode than returning another blockhash.
12-
/// Allows saving the random number for use in the future. It allows the contract to still access the blockhash even after 256 blocks.
10+
/// On L2 like Arbitrum block production is sporadic so block timestamp is more reliable than block number.
11+
/// Returns 0 when no random number is available.
12+
/// Allows saving the random number for use in the future. It allows the contract to retrieve the blockhash even after the time window.
1313
contract BlockHashRNG is IRNG {
14-
uint256 public immutable lookahead; // Minimal block distance between requesting and obtaining a random number.
15-
uint256 public requestBlock; // Block number of the current request
16-
mapping(uint256 block => uint256 number) public randomNumbers; // randomNumbers[block] is the random number for this block, 0 otherwise.
14+
// ************************************* //
15+
// * Storage * //
16+
// ************************************* //
1717

18-
constructor(uint256 _lookahead) {
19-
lookahead = _lookahead + lookahead;
18+
address public governor; // The address that can withdraw funds.
19+
address public consumer; // The address that can request random numbers.
20+
uint256 public immutable lookaheadTime; // Minimal time in seconds between requesting and obtaining a random number.
21+
uint256 public requestTimestamp; // Timestamp of the current request
22+
mapping(uint256 timestamp => uint256 number) public randomNumbers; // randomNumbers[timestamp] is the random number for this timestamp, 0 otherwise.
23+
24+
// ************************************* //
25+
// * Function Modifiers * //
26+
// ************************************* //
27+
28+
modifier onlyByGovernor() {
29+
require(governor == msg.sender, "Governor only");
30+
_;
31+
}
32+
33+
modifier onlyByConsumer() {
34+
require(consumer == msg.sender, "Consumer only");
35+
_;
36+
}
37+
38+
// ************************************* //
39+
// * Constructor * //
40+
// ************************************* //
41+
42+
/// @dev Constructor.
43+
/// @param _governor The Governor of the contract.
44+
/// @param _consumer The address that can request random numbers.
45+
/// @param _lookaheadTime The time lookahead in seconds for the random number.
46+
constructor(address _governor, address _consumer, uint256 _lookaheadTime) {
47+
governor = _governor;
48+
consumer = _consumer;
49+
lookaheadTime = _lookaheadTime;
50+
}
51+
52+
// ************************************* //
53+
// * Governance * //
54+
// ************************************* //
55+
56+
/// @dev Changes the governor of the contract.
57+
/// @param _governor The new governor.
58+
function changeGovernor(address _governor) external onlyByGovernor {
59+
governor = _governor;
60+
}
61+
62+
/// @dev Changes the consumer of the RNG.
63+
/// @param _consumer The new consumer.
64+
function changeConsumer(address _consumer) external onlyByGovernor {
65+
consumer = _consumer;
2066
}
2167

68+
// ************************************* //
69+
// * State Modifiers * //
70+
// ************************************* //
71+
2272
/// @dev Request a random number.
23-
function requestRandomness() external override {
24-
requestBlock = block.number;
73+
function requestRandomness() external override onlyByConsumer {
74+
requestTimestamp = block.timestamp;
2575
}
2676

2777
/// @dev Return the random number. If it has not been saved and is still computable compute it.
2878
/// @return randomNumber The random number or 0 if it is not ready or has not been requested.
29-
function receiveRandomness() external override returns (uint256 randomNumber) {
30-
uint256 expectedBlock = requestBlock;
31-
randomNumber = randomNumbers[expectedBlock];
79+
function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) {
80+
if (requestTimestamp == 0) return 0; // No request made
81+
82+
uint256 expectedTimestamp = requestTimestamp + lookaheadTime;
83+
84+
// Check if enough time has passed
85+
if (block.timestamp < expectedTimestamp) {
86+
return 0; // Not ready yet
87+
}
88+
89+
// Check if we already have a saved random number for this timestamp window
90+
randomNumber = randomNumbers[expectedTimestamp];
3291
if (randomNumber != 0) {
3392
return randomNumber;
3493
}
3594

36-
if (expectedBlock < block.number) {
37-
// The random number is not already set and can be.
38-
if (blockhash(expectedBlock) != 0x0) {
39-
// Normal case.
40-
randomNumber = uint256(blockhash(expectedBlock));
41-
} else {
42-
// The contract was not called in time. Fallback to returning previous blockhash.
43-
randomNumber = uint256(blockhash(block.number - 1));
44-
}
95+
// Use last block hash for randomness
96+
randomNumber = uint256(blockhash(block.number - 1));
97+
if (randomNumber != 0) {
98+
randomNumbers[expectedTimestamp] = randomNumber;
4599
}
46-
randomNumbers[expectedBlock] = randomNumber;
100+
return randomNumber;
101+
}
102+
103+
// ************************************* //
104+
// * View Functions * //
105+
// ************************************* //
106+
107+
/// @dev Check if randomness is ready to be received.
108+
/// @return ready True if randomness can be received.
109+
function isRandomnessReady() external view returns (bool ready) {
110+
if (requestTimestamp == 0) return false;
111+
return block.timestamp >= requestTimestamp + lookaheadTime;
112+
}
113+
114+
/// @dev Get the timestamp when randomness will be ready.
115+
/// @return readyTimestamp The timestamp when randomness will be available.
116+
function getRandomnessReadyTimestamp() external view returns (uint256 readyTimestamp) {
117+
if (requestTimestamp == 0) return 0;
118+
return requestTimestamp + lookaheadTime;
47119
}
48120
}

contracts/src/rng/RNGWithFallback.sol

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ contract RNGWithFallback is IRNG {
1313
// ************************************* //
1414

1515
address public governor; // Governor address
16+
address public consumer; // Consumer address
1617
IRNG[] public rngs; // List of RNG implementations
17-
uint256 public fallbackTimeout; // Number of blocks to wait before falling back to next RNG
18-
uint256 public requestBlock; // Block number of the current request
18+
uint256 public fallbackTimeoutSeconds; // Time in seconds to wait before falling back to next RNG
19+
uint256 public requestTimestamp; // Timestamp of the current request
1920
uint256 public currentRngIndex; // Index of the current RNG
2021
bool public isRequesting; // Whether a request is in progress
2122

@@ -25,6 +26,7 @@ contract RNGWithFallback is IRNG {
2526

2627
event RNGDefaultChanged(address indexed _newDefaultRng);
2728
event RNGFallback(uint256 _fromIndex, uint256 _toIndex);
29+
event RNGFailure();
2830
event RNGFallbackAdded(address indexed _rng);
2931
event RNGFallbackRemoved(address indexed _rng);
3032
event FallbackTimeoutChanged(uint256 _newTimeout);
@@ -34,14 +36,15 @@ contract RNGWithFallback is IRNG {
3436
// ************************************* //
3537

3638
/// @param _governor Governor address
37-
/// @param _fallbackTimeout Number of blocks to wait before falling back to next RNG
39+
/// @param _consumer Consumer address
40+
/// @param _fallbackTimeoutSeconds Time in seconds to wait before falling back to next RNG
3841
/// @param _defaultRng The default RNG
39-
constructor(address _governor, uint256 _fallbackTimeout, IRNG _defaultRng) {
42+
constructor(address _governor, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _defaultRng) {
4043
require(address(_defaultRng) != address(0), "Invalid default RNG");
41-
require(_fallbackTimeout > 0, "Invalid fallback timeout");
4244

4345
governor = _governor;
44-
fallbackTimeout = _fallbackTimeout;
46+
consumer = _consumer;
47+
fallbackTimeoutSeconds = _fallbackTimeoutSeconds;
4548
rngs.push(_defaultRng);
4649
}
4750

@@ -54,26 +57,31 @@ contract RNGWithFallback is IRNG {
5457
_;
5558
}
5659

60+
modifier onlyByConsumer() {
61+
require(msg.sender == consumer, "Consumer only");
62+
_;
63+
}
64+
5765
// ************************************* //
5866
// * State Modifiers * //
5967
// ************************************* //
6068

6169
/// @dev Request a random number from the default RNG
62-
function requestRandomness() external override {
70+
function requestRandomness() external override onlyByConsumer {
6371
require(!isRequesting, "Request already in progress");
64-
_requestRandomness();
72+
_requestRandomness(DEFAULT_RNG);
6573
}
6674

67-
function _requestRandomness() internal {
75+
function _requestRandomness(uint256 _rngIndex) internal {
6876
isRequesting = true;
69-
requestBlock = block.number;
70-
currentRngIndex = DEFAULT_RNG;
71-
rngs[DEFAULT_RNG].requestRandomness();
77+
requestTimestamp = block.timestamp;
78+
currentRngIndex = _rngIndex;
79+
rngs[_rngIndex].requestRandomness();
7280
}
7381

7482
/// @dev Receive the random number with fallback logic
7583
/// @return randomNumber Random Number
76-
function receiveRandomness() external override returns (uint256 randomNumber) {
84+
function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) {
7785
// Try to get random number from current RNG
7886
randomNumber = rngs[currentRngIndex].receiveRandomness();
7987

@@ -84,14 +92,17 @@ contract RNGWithFallback is IRNG {
8492
}
8593

8694
// If the timeout is exceeded, try next RNG
87-
if (block.number > requestBlock + fallbackTimeout) {
95+
if (block.timestamp > requestTimestamp + fallbackTimeoutSeconds) {
8896
uint256 nextIndex = currentRngIndex + 1;
8997

9098
// If we have another RNG to try, switch to it and request again
9199
if (nextIndex < rngs.length) {
92100
emit RNGFallback(currentRngIndex, nextIndex);
93101
currentRngIndex = nextIndex;
94-
rngs[nextIndex].requestRandomness();
102+
_requestRandomness(nextIndex);
103+
} else {
104+
// No more RNGs to try
105+
emit RNGFailure();
95106
}
96107
}
97108
return randomNumber;
@@ -104,10 +115,15 @@ contract RNGWithFallback is IRNG {
104115
/// @dev Change the governor
105116
/// @param _newGovernor Address of the new governor
106117
function changeGovernor(address _newGovernor) external onlyByGovernor {
107-
require(_newGovernor != address(0), "Invalid governor");
108118
governor = _newGovernor;
109119
}
110120

121+
/// @dev Change the consumer
122+
/// @param _consumer Address of the new consumer
123+
function changeConsumer(address _consumer) external onlyByGovernor {
124+
consumer = _consumer;
125+
}
126+
111127
/// @dev Change the default RNG
112128
/// @param _newDefaultRng Address of the new default RNG
113129
function changeDefaultRng(IRNG _newDefaultRng) external onlyByGovernor {
@@ -116,7 +132,7 @@ contract RNGWithFallback is IRNG {
116132
emit RNGDefaultChanged(address(_newDefaultRng));
117133

118134
// Take over any pending request
119-
_requestRandomness();
135+
_requestRandomness(DEFAULT_RNG);
120136
}
121137

122138
/// @dev Add a new RNG fallback
@@ -142,17 +158,18 @@ contract RNGWithFallback is IRNG {
142158
}
143159

144160
/// @dev Change the fallback timeout
145-
/// @param _newTimeout New timeout in blocks
146-
function changeFallbackTimeout(uint256 _newTimeout) external onlyByGovernor {
147-
require(_newTimeout > 0, "Invalid timeout");
148-
fallbackTimeout = _newTimeout;
149-
emit FallbackTimeoutChanged(_newTimeout);
161+
/// @param _fallbackTimeoutSeconds New timeout in seconds
162+
function changeFallbackTimeout(uint256 _fallbackTimeoutSeconds) external onlyByGovernor {
163+
fallbackTimeoutSeconds = _fallbackTimeoutSeconds;
164+
emit FallbackTimeoutChanged(_fallbackTimeoutSeconds);
150165
}
151166

152-
/// @dev Drop the pending request.
167+
/// @dev Emergency reset the RNG.
153168
/// Useful for the governor to ensure that re-requesting a random number will not be blocked by a previous request.
154-
function dropPendingRequest() external onlyByGovernor {
169+
function emergencyReset() external onlyByGovernor {
155170
isRequesting = false;
171+
requestTimestamp = 0;
172+
currentRngIndex = DEFAULT_RNG;
156173
}
157174

158175
// ************************************* //

contracts/test/rng/index.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { deployments, ethers, network } from "hardhat";
33
import { IncrementalNG, BlockHashRNG, ChainlinkRNG, ChainlinkVRFCoordinatorV2Mock } from "../../typechain-types";
44

55
const initialNg = 424242;
6-
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
76

87
describe("IncrementalNG", async () => {
98
let rng: IncrementalNG;
@@ -28,15 +27,48 @@ describe("BlockHashRNG", async () => {
2827
let rng: BlockHashRNG;
2928

3029
beforeEach("Setup", async () => {
31-
const rngFactory = await ethers.getContractFactory("BlockHashRNG");
32-
rng = (await rngFactory.deploy(1)) as BlockHashRNG;
30+
const [deployer] = await ethers.getSigners();
31+
await deployments.delete("BlockHashRNG");
32+
await deployments.deploy("BlockHashRNG", {
33+
from: deployer.address,
34+
args: [deployer.address, deployer.address, 10], // governor, consumer, lookaheadTime (seconds)
35+
});
36+
rng = await ethers.getContract<BlockHashRNG>("BlockHashRNG");
3337
});
3438

35-
it("Should return a non-zero number for a block number", async () => {
36-
const tx = await rng.receiveRandomness();
37-
const trace = await network.provider.send("debug_traceTransaction", [tx.hash]);
38-
const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`));
39-
expect(rn).to.not.equal(0);
39+
it("Should return a non-zero number after requesting and waiting", async () => {
40+
// First request randomness
41+
await rng.requestRandomness();
42+
43+
// Check that it's not ready yet
44+
expect(await rng.isRandomnessReady()).to.be.false;
45+
46+
// Advance time by 10 seconds (the lookahead time)
47+
await network.provider.send("evm_increaseTime", [10]);
48+
await network.provider.send("evm_mine");
49+
50+
// Now it should be ready
51+
expect(await rng.isRandomnessReady()).to.be.true;
52+
53+
// Get the random number
54+
const randomNumber = await rng.receiveRandomness.staticCall();
55+
expect(randomNumber).to.not.equal(0);
56+
});
57+
58+
it("Should return 0 if randomness not requested", async () => {
59+
const randomNumber = await rng.receiveRandomness.staticCall();
60+
expect(randomNumber).to.equal(0);
61+
});
62+
63+
it("Should return 0 if not enough time has passed", async () => {
64+
await rng.requestRandomness();
65+
66+
// Don't advance time enough
67+
await network.provider.send("evm_increaseTime", [5]); // Only 5 seconds
68+
await network.provider.send("evm_mine");
69+
70+
const randomNumber = await rng.receiveRandomness.staticCall();
71+
expect(randomNumber).to.equal(0);
4072
});
4173
});
4274

0 commit comments

Comments
 (0)