diff --git a/contracts/RLPReader.sol b/contracts/RLPReader.sol new file mode 100644 index 0000000..ccccca3 --- /dev/null +++ b/contracts/RLPReader.sol @@ -0,0 +1,358 @@ +/* + * @author Hamdi Allam hamdi.allam97@gmail.com + * Please reach out with any questions or concerns + * https://github.com/hamdiallam/Solidity-RLP/blob/e681e25a376dbd5426b509380bc03446f05d0f97/contracts/RLPReader.sol + */ +pragma solidity ^0.8.0; + +library RLPReader { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint len; + uint memPtr; + } + + struct Iterator { + RLPItem item; // Item that's being iterated over. + uint nextPtr; // Position of the next item in the list. + } + + /* + * @dev Returns the next element in the iteration. Reverts if it has not next element. + * @param self The iterator. + * @return The next element in the iteration. + */ + function next(Iterator memory self) internal pure returns (RLPItem memory) { + require(hasNext(self)); + + uint ptr = self.nextPtr; + uint itemLength = _itemLength(ptr); + self.nextPtr = ptr + itemLength; + + return RLPItem(itemLength, ptr); + } + + /* + * @dev Returns true if the iteration has more elements. + * @param self The iterator. + * @return true if the iteration has more elements. + */ + function hasNext(Iterator memory self) internal pure returns (bool) { + RLPItem memory item = self.item; + return self.nextPtr < item.memPtr + item.len; + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + uint memPtr; + assembly { + memPtr := add(item, 0x20) + } + + return RLPItem(item.length, memPtr); + } + + /* + * @dev Create an iterator. Reverts if item is not a list. + * @param self The RLP item. + * @return An 'Iterator' over the item. + */ + function iterator(RLPItem memory self) internal pure returns (Iterator memory) { + require(isList(self)); + + uint ptr = self.memPtr + _payloadOffset(self.memPtr); + return Iterator(self, ptr); + } + + /* + * @param the RLP item. + */ + function rlpLen(RLPItem memory item) internal pure returns (uint) { + return item.len; + } + + /* + * @param the RLP item. + * @return (memPtr, len) pair: location of the item's payload in memory. + */ + function payloadLocation(RLPItem memory item) internal pure returns (uint, uint) { + uint offset = _payloadOffset(item.memPtr); + uint memPtr = item.memPtr + offset; + uint len = item.len - offset; // data length + return (memPtr, len); + } + + /* + * @param the RLP item. + */ + function payloadLen(RLPItem memory item) internal pure returns (uint) { + (, uint len) = payloadLocation(item); + return len; + } + + /* + * @param the RLP item containing the encoded list. + */ + function toList(RLPItem memory item) internal pure returns (RLPItem[] memory) { + require(isList(item)); + + uint items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + + uint memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint dataLen; + for (uint i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + + return result; + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + if (item.len == 0) return false; + + uint8 byte0; + uint memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) + return false; + return true; + } + + /* + * @dev A cheaper version of keccak256(toRlpBytes(item)) that avoids copying memory. + * @return keccak256 hash of RLP encoded bytes. + */ + function rlpBytesKeccak256(RLPItem memory item) internal pure returns (bytes32) { + uint256 ptr = item.memPtr; + uint256 len = item.len; + bytes32 result; + assembly { + result := keccak256(ptr, len) + } + return result; + } + + /* + * @dev A cheaper version of keccak256(toBytes(item)) that avoids copying memory. + * @return keccak256 hash of the item payload. + */ + function payloadKeccak256(RLPItem memory item) internal pure returns (bytes32) { + (uint memPtr, uint len) = payloadLocation(item); + bytes32 result; + assembly { + result := keccak256(memPtr, len) + } + return result; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + if (result.length == 0) return result; + + uint ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + // any non-zero byte except "0x80" is considered true + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1); + uint result; + uint memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + + // SEE Github Issue #5. + // Summary: Most commonly used RLP libraries (i.e Geth) will encode + // "0" as "0x80" instead of as "0". We handle this edge case explicitly + // here. + if (result == 0 || result == STRING_SHORT_START) { + return false; + } else { + return true; + } + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix + require(item.len == 21); + + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint) { + require(item.len > 0 && item.len <= 33); + + (uint memPtr, uint len) = payloadLocation(item); + + uint result; + assembly { + result := mload(memPtr) + + // shfit to the correct location if neccesary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint) { + // one byte prefix + require(item.len == 33); + + uint result; + uint memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + require(item.len > 0); + + (uint memPtr, uint len) = payloadLocation(item); + bytes memory result = new bytes(len); + + uint destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(memPtr, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) private pure returns (uint) { + if (item.len == 0) return 0; + + uint count = 0; + uint currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint memPtr) private pure returns (uint) { + uint itemLen; + uint byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) + itemLen = 1; + + else if (byte0 < STRING_LONG_START) + itemLen = byte0 - STRING_SHORT_START + 1; + + else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } + + else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint memPtr) private pure returns (uint) { + uint byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) + return 0; + else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) + return 1; + else if (byte0 < LIST_SHORT_START) // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + else + return byte0 - (LIST_LONG_START - 1) + 1; + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy(uint src, uint dest, uint len) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + if (len > 0) { + // left over bytes. Mask is used to remove unwanted bytes from the word + uint mask = 256 ** (WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } + } +} \ No newline at end of file diff --git a/contracts/RandaoLib.sol b/contracts/RandaoLib.sol new file mode 100644 index 0000000..f31a015 --- /dev/null +++ b/contracts/RandaoLib.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./RLPReader.sol"; + +library RandaoLib { + using RLPReader for RLPReader.RLPItem; + using RLPReader for RLPReader.Iterator; + using RLPReader for bytes; + + function getRandaoFromHeader(RLPReader.RLPItem memory item) pure internal returns (bytes32) { + RLPReader.Iterator memory iterator = item.iterator(); + // mixDigest is at item 13 (0-base index) + for (uint256 i = 0; i < 13; i++) { + iterator.next(); + } + + return bytes32(iterator.next().toUint()); + } + + function verifyHeaderAndGetRandao(bytes32 headerHash, bytes memory headerRlpBytes) pure internal returns (bytes32) { + RLPReader.RLPItem memory item = headerRlpBytes.toRlpItem(); + require(headerHash == item.rlpBytesKeccak256(), "header hash mismatch"); + return getRandaoFromHeader(item); + } +} \ No newline at end of file diff --git a/contracts/TestRandao.sol b/contracts/TestRandao.sol new file mode 100644 index 0000000..0f4492c --- /dev/null +++ b/contracts/TestRandao.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./RandaoLib.sol"; + +contract TestRandao { + function verifyHeaderAndGetRandao(bytes32 headerHash, bytes memory headerRlpBytes) public pure returns (bytes32) { + return RandaoLib.verifyHeaderAndGetRandao(headerHash, headerRlpBytes); + } + + function verifyHistoricalRandao(uint256 blockNumber, bytes memory proof) public view returns (bytes32) { + // TODO; + } + + // Test vector of header at 19010852 + // 0x8576cd4900e56c1214e2f32fbb194c0ebddde8cffd243194187977583d712aa5 + // Raw: 0xf90232a00607c9891abee48a5d70948d61d41ef7cabe554a8dd1aec49c6322ba7e2fab25a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479495222290dd7278aa3ddd389cc1e1d165cc4bafe5a00f90554fb8e825ce12c6ad7f0d5564e0197d6b41b4d6cf2a65175373795ef1faa0ce8641939cdf1811521940f78609856d43a8eaf4e7b5c0a48e24c242cd57ec4ba052c2389675794511f8787f7a999019d9bfe9bfa9c6ec3b8544592241387d52aab9010010a1d45047215b70ca08a790aa40920729bb9280ea453316106f832056a109a0193404ed0c01026300305933a870495813258a488813a4b8669d4644c0682180910ab60909c48d0a6b83c04e022c34f06199085db4fe19811143e213c832cd7209850550260ec14204c22a0c64646f020a30a8c01e008400c61621d4420850566032045603891962784d03d201620734430308930fb9130804c201437b700c622f43b1da4c80600365c04c86f8800cc40602a42103bc00222131a90601ce040055044e035af412610422ba2310c80a4c2dc023729669081405df1546c10120130098e001394c0102286c50252810ad44dea07930211814440a009c3c80630c218084012212498401c9c38083b7c8408465a4bef38f6265617665726275696c642e6f7267a084376d868db109d93e3062b04daa8a4f50e8ac872a0807eb740d306a395dc7d68800000000000000008503e34e2411a03e72aaa4c11c1c04da24b4e2e40709b8a0795e877997921ab5806acd890c1054 + // { + // baseFeePerGas: "0x3e34e2411", + // difficulty: "0x0", + // extraData: "0x6265617665726275696c642e6f7267", + // gasLimit: "0x1c9c380", + // gasUsed: "0xb7c840", + // hash: "0x8576cd4900e56c1214e2f32fbb194c0ebddde8cffd243194187977583d712aa5", + // logsBloom: "0x10a1d45047215b70ca08a790aa40920729bb9280ea453316106f832056a109a0193404ed0c01026300305933a870495813258a488813a4b8669d4644c0682180910ab60909c48d0a6b83c04e022c34f06199085db4fe19811143e213c832cd7209850550260ec14204c22a0c64646f020a30a8c01e008400c61621d4420850566032045603891962784d03d201620734430308930fb9130804c201437b700c622f43b1da4c80600365c04c86f8800cc40602a42103bc00222131a90601ce040055044e035af412610422ba2310c80a4c2dc023729669081405df1546c10120130098e001394c0102286c50252810ad44dea07930211814440a009c3c80630c21", + // miner: "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5", + // mixHash: "0x84376d868db109d93e3062b04daa8a4f50e8ac872a0807eb740d306a395dc7d6", + // nonce: "0x0000000000000000", + // number: "0x1221249", + // parentHash: "0x0607c9891abee48a5d70948d61d41ef7cabe554a8dd1aec49c6322ba7e2fab25", + // receiptsRoot: "0x52c2389675794511f8787f7a999019d9bfe9bfa9c6ec3b8544592241387d52aa", + // sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + // size: "0x24e", + // stateRoot: "0x0f90554fb8e825ce12c6ad7f0d5564e0197d6b41b4d6cf2a65175373795ef1fa", + // timestamp: "0x65a4bef3", + // totalDifficulty: "0xc70d815d562d3cfa955", + // transactionsRoot: "0xce8641939cdf1811521940f78609856d43a8eaf4e7b5c0a48e24c242cd57ec4b", + // withdrawalsRoot: "0x3e72aaa4c11c1c04da24b4e2e40709b8a0795e877997921ab5806acd890c1054" + // } + // Result = 0x84376d868db109d93e3062b04daa8a4f50e8ac872a0807eb740d306a395dc7d6 +} \ No newline at end of file