From 9bc79e34bc54d47b00540a559ca5e6a15e995882 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 5 Sep 2024 13:30:17 +0100 Subject: [PATCH] feat: initial implementation of a historical beacon RNG --- contracts/libraries/RLPReader.sol | 435 ++++++++++++++++++ .../standard/rng/BeaconHistoricalRNG.sol | 101 ++++ contracts/standard/rng/IRNG.sol | 31 ++ 3 files changed, 567 insertions(+) create mode 100644 contracts/libraries/RLPReader.sol create mode 100644 contracts/standard/rng/BeaconHistoricalRNG.sol create mode 100644 contracts/standard/rng/IRNG.sol diff --git a/contracts/libraries/RLPReader.sol b/contracts/libraries/RLPReader.sol new file mode 100644 index 00000000..83570d1f --- /dev/null +++ b/contracts/libraries/RLPReader.sol @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/** + * @custom:attribution https://github.com/ethereum-optimism/optimism + * @custom:attribution https://github.com/hamdiallam/Solidity-RLP + * @title RLPReader + * @notice RLPReader is a library for parsing RLP-encoded byte arrays into Solidity types. Adapted + * from Solidity-RLP (https://github.com/hamdiallam/Solidity-RLP) by Hamdi Allam with + * various tweaks to improve readability. + */ +library RLPReader { + /** + * Custom pointer type to avoid confusion between pointers and uint256s. + */ + type MemoryPointer is uint256; + + /** + * @notice RLP item types. + * + * @custom:value DATA_ITEM Represents an RLP data item (NOT a list). + * @custom:value LIST_ITEM Represents an RLP list item. + */ + enum RLPItemType { + DATA_ITEM, + LIST_ITEM + } + + /** + * @notice Struct representing an RLP item. + * + * @custom:field length Length of the RLP item. + * @custom:field ptr Pointer to the RLP item in memory. + */ + struct RLPItem { + uint256 length; + MemoryPointer ptr; + } + + /** + * @notice Max list length that this library will accept. + */ + uint256 internal constant MAX_LIST_LENGTH = 32; + + /** + * @notice Converts bytes to a reference to memory position and length. + * + * @param _in Input bytes to convert. + * + * @return Output memory reference. + */ + function toRLPItem( + bytes memory _in + ) internal pure returns (RLPItem memory) { + // Empty arrays are not RLP items. + require( + _in.length > 0, + "RLPReader: length of an RLP item must be greater than zero to be decodable" + ); + + MemoryPointer ptr; + assembly { + ptr := add(_in, 32) + } + + return RLPItem({length: _in.length, ptr: ptr}); + } + + /** + * @notice Reads an RLP list value into a list of RLP items. + * + * @param _in RLP list value. + * + * @return Decoded RLP list items. + */ + function readList( + RLPItem memory _in + ) internal pure returns (RLPItem[] memory) { + ( + uint256 listOffset, + uint256 listLength, + RLPItemType itemType + ) = _decodeLength(_in); + + require( + itemType == RLPItemType.LIST_ITEM, + "RLPReader: decoded item type for list is not a list item" + ); + + require( + listOffset + listLength == _in.length, + "RLPReader: list item has an invalid data remainder" + ); + + // Solidity in-memory arrays can't be increased in size, but *can* be decreased in size by + // writing to the length. Since we can't know the number of RLP items without looping over + // the entire input, we'd have to loop twice to accurately size this array. It's easier to + // simply set a reasonable maximum list length and decrease the size before we finish. + RLPItem[] memory out = new RLPItem[](MAX_LIST_LENGTH); + + uint256 itemCount = 0; + uint256 offset = listOffset; + while (offset < _in.length) { + (uint256 itemOffset, uint256 itemLength, ) = _decodeLength( + RLPItem({ + length: _in.length - offset, + ptr: MemoryPointer.wrap( + MemoryPointer.unwrap(_in.ptr) + offset + ) + }) + ); + + // We don't need to check itemCount < out.length explicitly because Solidity already + // handles this check on our behalf, we'd just be wasting gas. + out[itemCount] = RLPItem({ + length: itemLength + itemOffset, + ptr: MemoryPointer.wrap(MemoryPointer.unwrap(_in.ptr) + offset) + }); + + itemCount += 1; + offset += itemOffset + itemLength; + } + + // Decrease the array size to match the actual item count. + assembly { + mstore(out, itemCount) + } + + return out; + } + + /** + * @notice Reads an RLP list value into a list of RLP items. + * + * @param _in RLP list value. + * + * @return Decoded RLP list items. + */ + function readList( + bytes memory _in + ) internal pure returns (RLPItem[] memory) { + return readList(toRLPItem(_in)); + } + + /** + * @notice Reads an RLP bytes value into bytes. + * + * @param _in RLP bytes value. + * + * @return Decoded bytes. + */ + function readBytes( + RLPItem memory _in + ) internal pure returns (bytes memory) { + ( + uint256 itemOffset, + uint256 itemLength, + RLPItemType itemType + ) = _decodeLength(_in); + + require( + itemType == RLPItemType.DATA_ITEM, + "RLPReader: decoded item type for bytes is not a data item" + ); + + require( + _in.length == itemOffset + itemLength, + "RLPReader: bytes value contains an invalid remainder" + ); + + return _copy(_in.ptr, itemOffset, itemLength); + } + + /** + * @notice Reads an RLP bytes value into bytes. + * + * @param _in RLP bytes value. + * + * @return Decoded bytes. + */ + function readBytes(bytes memory _in) internal pure returns (bytes memory) { + return readBytes(toRLPItem(_in)); + } + + /** + * @notice Reads the raw bytes of an RLP item. + * + * @param _in RLP item to read. + * + * @return Raw RLP bytes. + */ + function readRawBytes( + RLPItem memory _in + ) internal pure returns (bytes memory) { + return _copy(_in.ptr, 0, _in.length); + } + + /** + * @notice Decodes the length of an RLP item. + * + * @param _in RLP item to decode. + * + * @return Offset of the encoded data. + * @return Length of the encoded data. + * @return RLP item type (LIST_ITEM or DATA_ITEM). + */ + function _decodeLength( + RLPItem memory _in + ) private pure returns (uint256, uint256, RLPItemType) { + // Short-circuit if there's nothing to decode, note that we perform this check when + // the user creates an RLP item via toRLPItem, but it's always possible for them to bypass + // that function and create an RLP item directly. So we need to check this anyway. + require( + _in.length > 0, + "RLPReader: length of an RLP item must be greater than zero to be decodable" + ); + + MemoryPointer ptr = _in.ptr; + uint256 prefix; + assembly { + prefix := byte(0, mload(ptr)) + } + + if (prefix <= 0x7f) { + // Single byte. + return (0, 1, RLPItemType.DATA_ITEM); + } else if (prefix <= 0xb7) { + // Short string. + + // slither-disable-next-line variable-scope + uint256 strLen = prefix - 0x80; + + require( + _in.length > strLen, + "RLPReader: length of content must be greater than string length (short string)" + ); + + bytes1 firstByteOfContent; + assembly { + firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) + } + + require( + strLen != 1 || firstByteOfContent >= 0x80, + "RLPReader: invalid prefix, single byte < 0x80 are not prefixed (short string)" + ); + + return (1, strLen, RLPItemType.DATA_ITEM); + } else if (prefix <= 0xbf) { + // Long string. + uint256 lenOfStrLen = prefix - 0xb7; + + require( + _in.length > lenOfStrLen, + "RLPReader: length of content must be > than length of string length (long string)" + ); + + bytes1 firstByteOfContent; + assembly { + firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) + } + + require( + firstByteOfContent != 0x00, + "RLPReader: length of content must not have any leading zeros (long string)" + ); + + uint256 strLen; + assembly { + strLen := shr(sub(256, mul(8, lenOfStrLen)), mload(add(ptr, 1))) + } + + require( + strLen > 55, + "RLPReader: length of content must be greater than 55 bytes (long string)" + ); + + require( + _in.length > lenOfStrLen + strLen, + "RLPReader: length of content must be greater than total length (long string)" + ); + + return (1 + lenOfStrLen, strLen, RLPItemType.DATA_ITEM); + } else if (prefix <= 0xf7) { + // Short list. + // slither-disable-next-line variable-scope + uint256 listLen = prefix - 0xc0; + + require( + _in.length > listLen, + "RLPReader: length of content must be greater than list length (short list)" + ); + + return (1, listLen, RLPItemType.LIST_ITEM); + } else { + // Long list. + uint256 lenOfListLen = prefix - 0xf7; + + require( + _in.length > lenOfListLen, + "RLPReader: length of content must be > than length of list length (long list)" + ); + + bytes1 firstByteOfContent; + assembly { + firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) + } + + require( + firstByteOfContent != 0x00, + "RLPReader: length of content must not have any leading zeros (long list)" + ); + + uint256 listLen; + assembly { + listLen := shr( + sub(256, mul(8, lenOfListLen)), + mload(add(ptr, 1)) + ) + } + + require( + listLen > 55, + "RLPReader: length of content must be greater than 55 bytes (long list)" + ); + + require( + _in.length > lenOfListLen + listLen, + "RLPReader: length of content must be greater than total length (long list)" + ); + + return (1 + lenOfListLen, listLen, RLPItemType.LIST_ITEM); + } + } + + /** + * @notice Copies the bytes from a memory location. + * + * @param _src Pointer to the location to read from. + * @param _offset Offset to start reading from. + * @param _length Number of bytes to read. + * + * @return Copied bytes. + */ + function _copy( + MemoryPointer _src, + uint256 _offset, + uint256 _length + ) private pure returns (bytes memory) { + bytes memory out = new bytes(_length); + if (_length == 0) { + return out; + } + + // Mostly based on Solidity's copy_memory_to_memory: + // solhint-disable max-line-length + // https://github.com/ethereum/solidity/blob/34dd30d71b4da730488be72ff6af7083cf2a91f6/libsolidity/codegen/YulUtilFunctions.cpp#L102-L114 + uint256 src = MemoryPointer.unwrap(_src) + _offset; + assembly { + let dest := add(out, 32) + let i := 0 + for { + + } lt(i, _length) { + i := add(i, 32) + } { + mstore(add(dest, i), mload(add(src, i))) + } + + if gt(i, _length) { + mstore(add(dest, _length), 0) + } + } + + return out; + } + + /// @notice Copy RLP value, padded to `maxLen` + function _copyPadded( + MemoryPointer _src, + uint256 _offset, + uint256 _length, + uint256 maxLen + ) private pure returns (bytes memory) { + require(_length <= maxLen, "RLPReader: pad overflow"); + bytes memory out = new bytes(maxLen); + if (_length == 0) { + return out; + } + + // Mostly based on Solidity's copy_memory_to_memory: + // solhint-disable max-line-length + // https://github.com/ethereum/solidity/blob/34dd30d71b4da730488be72ff6af7083cf2a91f6/libsolidity/codegen/YulUtilFunctions.cpp#L102-L114 + uint256 src = MemoryPointer.unwrap(_src) + _offset; + assembly { + let padLen := sub(maxLen, _length) + let dest := add(add(out, 32), padLen) + let i := 0 + for { + + } lt(i, _length) { + i := add(i, 32) + } { + mstore(add(dest, i), mload(add(src, i))) + } + + if gt(i, _length) { + mstore(add(dest, _length), 0) + } + } + + return out; + } + + /// @notice Read uint256 RLP value + function readUint256(RLPItem memory _in) internal pure returns (uint256) { + ( + uint256 itemOffset, + uint256 itemLength, + RLPItemType itemType + ) = _decodeLength(_in); + require( + itemType == RLPItemType.DATA_ITEM, + "RLPReader: decoded item type for bytes is not a data item" + ); + require( + _in.length == itemOffset + itemLength, + "RLPReader: bytes value contains an invalid remainder" + ); + require(itemLength <= 32, "RLPReader: number too big"); + + return + uint256(bytes32(_copyPadded(_in.ptr, itemOffset, itemLength, 32))); + } +} \ No newline at end of file diff --git a/contracts/standard/rng/BeaconHistoricalRNG.sol b/contracts/standard/rng/BeaconHistoricalRNG.sol new file mode 100644 index 00000000..b9336897 --- /dev/null +++ b/contracts/standard/rng/BeaconHistoricalRNG.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.26; + +import {IRNG} from "./IRNG.sol"; +import {RLPReader} from "../../libraries/RLPReader.sol"; + +/** + * @title Random Number Generator using `prevrandao` from the historical beacon chain block headers. + * @dev The random numbers are provided in the RLP-encoded block headers and validated onchain against the block hashes. + * Adapted from kevincharm's https://github.com/kevincharm/randao-accessor/blob/312637b99be8d85f1afc6ca878c84adbb78fc7a6/contracts/RandaoAccessor.sol + */ +contract BeaconHistoricalRNG is IRNG { + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + + uint256 public constant LOOKAHEAD = 132; // Number of blocks that has to pass before obtaining the random number. 4 epochs + 4 slots, according to EIP-4399. + + IRNG public blockhashRNGFallback; // Address of blockhashRNGFallback to fall back on. + mapping(uint256 blockNumber => uint256 randomNumber) public randomNumbers; // The random number for this block, 0 otherwise. + + /** + * @dev Emitted when a block header is requested. + * @param _blockNumber The block number of the header requested. + */ + event Request(uint256 _blockNumber); + + /** + * @dev Constructor. + * @param _blockhashRNGFallback The blockhash RNG deployed contract address. + */ + constructor(IRNG _blockhashRNGFallback) { + blockhashRNGFallback = _blockhashRNGFallback; + } + + function contribute(uint256 _block) public payable override {} // Not used + + /** + * @dev Request a random number. + * @param _block Block number of the header requested for verification by `verifyRecent()`. + */ + function requestRN(uint256 _block) public payable override { + emit Request(_block + LOOKAHEAD); + } + + /** + * @dev Return the random number. If it has not been saved and is still computable compute it. + * @param _block Block number requested. + * @return rn Random Number. If the number is not ready or has not been requested 0 instead. + */ + function getRN(uint256 _block) public override returns (uint256 rn) { + if (block.difficulty <= 2**64) { + // Pre-Merge + rn = blockhashRNGFallback.getRN(_block); + } else { + // Post-Merge + if (block.number > _block + LOOKAHEAD) { + rn = randomNumbers[_block + LOOKAHEAD]; + } + } + } + + /** + * @dev Get an uncorrelated random number. + * @param _block Block number requested. + * @return rn Random Number. If the number is not ready or has not been required 0 instead. + */ + function getUncorrelatedRN(uint256 _block) public override returns (uint256 rn) { + rn = getRN(_block); + if (rn != 0) { + rn = uint256(keccak256(abi.encodePacked(msg.sender, rn))); + } + } + + /** + * @notice Verify a recent block header and return the prevrandao value + * @param _blockHeaderRLP RLP-encoded block header + * @return Verified prevrandao + */ + function verifyRecent(bytes calldata _blockHeaderRLP) public returns (uint256) { + RLPReader.RLPItem[] memory blockHeader = _blockHeaderRLP.readList(); + uint256 _inputBlockNumber = blockHeader[8].readUint256(); + require( + _inputBlockNumber < block.number, + "Input block must be older than current" + ); + require( + (_inputBlockNumber >= (block.number - 256)) && + (_inputBlockNumber < block.number), + "Block too old" + ); + bytes32 targetBlockHash = blockhash(_inputBlockNumber); + require( + targetBlockHash == keccak256(_blockHeaderRLP), + "RLP does not match blockhash" + ); + uint256 randao = blockHeader[13].readUint256(); + randomNumbers[_inputBlockNumber] = randao; + return randao; + } +} diff --git a/contracts/standard/rng/IRNG.sol b/contracts/standard/rng/IRNG.sol new file mode 100644 index 00000000..cc472419 --- /dev/null +++ b/contracts/standard/rng/IRNG.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IRNG { + /** + * @dev Contribute to the reward of a random number. + * @param _block Block the random number is linked to. + */ + function contribute(uint256 _block) external payable; + + /** + * @dev Request a random number. + * @param _block Block linked to the request. + */ + function requestRN(uint256 _block) external payable; + + /** + * @dev Get the random number. + * @param _block Block the random number is linked to. + * @return RN Random Number. If the number is not ready or has not been required 0 instead. + */ + function getRN(uint256 _block) external returns (uint256); + + /** + * @dev Get a uncorrelated random number. Act like getRN but give a different number for each sender. + * This is to prevent users from getting correlated numbers. + * @param _block Block the random number is linked to. + * @return RN Random Number. If the number is not ready or has not been required 0 instead. + */ + function getUncorrelatedRN(uint256 _block) external returns (uint256); +}