From 48fcc27bee8fcc19862b1a2d096b0a84754c0b4a Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Thu, 27 Mar 2025 17:51:46 +0000 Subject: [PATCH 01/16] RLP Writing of eip1559 tx --- .../unit/libraries/TestEip1559RlpEncoder.sol | 18 ++ contracts/src/libraries/Eip1559RlpEncoder.sol | 52 +++++ contracts/src/libraries/RlpWriter.sol | 191 ++++++++++++++++++ .../messaging/l1/v1/L1MessageServiceV1.sol | 7 +- .../messaging/l2/v1/L2MessageServiceV1.sol | 7 +- contracts/src/rollup/LineaRollup.sol | 4 +- .../src/security/pausing/PauseManager.sol | 6 +- contracts/src/verifiers/PlonkVerifierDev.sol | 4 +- contracts/test/foundry/LineaRollup.t.sol | 17 +- .../withCalldata.json | 27 +++ .../withoutCalldata.json | 27 +++ .../test/hardhat/common/helpers/hashing.ts | 4 +- contracts/test/hardhat/common/types.ts | 13 ++ .../hardhat/libraries/Eip1559RlpEncoder.ts | 57 ++++++ 14 files changed, 401 insertions(+), 33 deletions(-) create mode 100644 contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol create mode 100644 contracts/src/libraries/Eip1559RlpEncoder.sol create mode 100644 contracts/src/libraries/RlpWriter.sol create mode 100644 contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldata.json create mode 100644 contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withoutCalldata.json create mode 100644 contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts diff --git a/contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol b/contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol new file mode 100644 index 000000000..94b1565d5 --- /dev/null +++ b/contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.28; + +import { Eip1559RlpEncoder } from "../../../libraries/Eip1559RlpEncoder.sol"; + +contract TestEip1559RlpEncoder { + uint256 chainId; + + constructor(uint256 _chainId) { + chainId = _chainId; + } + + function encodeEip1559Transaction( + Eip1559RlpEncoder.Eip1559Transaction calldata _transaction + ) external view returns (bytes memory rlpEncodedTransaction, bytes32 transactionHash) { + (rlpEncodedTransaction, transactionHash) = Eip1559RlpEncoder.encodeEIP1559Tx(chainId, _transaction); + } +} diff --git a/contracts/src/libraries/Eip1559RlpEncoder.sol b/contracts/src/libraries/Eip1559RlpEncoder.sol new file mode 100644 index 000000000..045585cf5 --- /dev/null +++ b/contracts/src/libraries/Eip1559RlpEncoder.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * @title Library for RLP Encoding EIP-1559 transactions. + * @author ConsenSys Software Inc. + * @custom:security-contact security-report@linea.build + */ +pragma solidity ^0.8.28; + +import { RlpWriter } from "./RlpWriter.sol"; + +library Eip1559RlpEncoder { + /// @dev chainId is defined in the function and access list is not encoded. + struct Eip1559Transaction { + uint256 nonce; + uint256 maxPriorityFeePerGas; + uint256 maxFeePerGas; + uint256 gasLimit; + address to; + uint256 value; + bytes input; + uint8 v; + uint256 r; + uint256 s; + } + + function encodeEIP1559Tx( + uint256 _chainId, + Eip1559Transaction memory _transaction + ) internal pure returns (bytes memory rlpEncodedTransaction, bytes32 transactionHash) { + bytes[] memory fields = new bytes[](12); + + fields[0] = RlpWriter._encodeUint(_chainId); + fields[1] = RlpWriter._encodeUint(_transaction.nonce); + fields[2] = RlpWriter._encodeUint(_transaction.maxPriorityFeePerGas); + fields[3] = RlpWriter._encodeUint(_transaction.maxFeePerGas); + fields[4] = RlpWriter._encodeUint(_transaction.gasLimit); + fields[5] = RlpWriter._encodeAddress(_transaction.to); + fields[6] = RlpWriter._encodeUint(_transaction.value); + fields[7] = RlpWriter._encodeBytes(_transaction.input); + fields[8] = RlpWriter._encodeList(new bytes[](0)); // AccessList empty on purpose. + fields[9] = RlpWriter._encodeUint(_transaction.v); + fields[10] = RlpWriter._encodeUint(_transaction.r); + fields[11] = RlpWriter._encodeUint(_transaction.s); + + bytes memory encodedList = RlpWriter._encodeList(fields); + + // Prepend type byte 0x02 + rlpEncodedTransaction = abi.encodePacked(hex"02", encodedList); + transactionHash = keccak256(rlpEncodedTransaction); + } +} diff --git a/contracts/src/libraries/RlpWriter.sol b/contracts/src/libraries/RlpWriter.sol new file mode 100644 index 000000000..5f4c4dc3c --- /dev/null +++ b/contracts/src/libraries/RlpWriter.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * @title Library for RLP Encoding data. + * @author ConsenSys Software Inc. + * @custom:security-contact security-report@linea.build + */ +pragma solidity ^0.8.28; + +/// @custom:attribution https://github.com/bakaoh/solidity-rlp-encode +/// @title RLPWriter +/// @author RLPWriter is a library for encoding Solidity types to RLP bytes. Adapted from Bakaoh's +/// RLPEncode library (https://github.com/bakaoh/solidity-rlp-encode) with +/// modifications to improve legibility and gas consumption. +library RlpWriter { + function _encodeBytes(bytes memory _bytesIn) internal pure returns (bytes memory bytesOut) { + if (_bytesIn.length == 1 && uint8(_bytesIn[0]) < 0x80) { + return _bytesIn; // Return as-is for single-byte < 0x80 + } + + bytes memory lengthPrefix = _writeLength(_bytesIn.length, 128); + uint256 prefixLen = lengthPrefix.length; + uint256 dataLen = _bytesIn.length; + uint256 totalLen = prefixLen + dataLen; + + assembly { + // Allocate output buffer + bytesOut := mload(0x40) + mstore(bytesOut, totalLen) + + // Copy prefix + let dest := add(bytesOut, 0x20) + let src := add(lengthPrefix, 0x20) + mcopy(dest, src, prefixLen) + + // Copy input bytes + dest := add(dest, prefixLen) + src := add(_bytesIn, 0x20) + mcopy(dest, src, dataLen) + + // Move free memory pointer + mstore(0x40, add(add(bytesOut, 0x20), totalLen)) + } + } + + function _encodeUint(uint256 _uintIn) internal pure returns (bytes memory uintBytes) { + uintBytes = _encodeBytes(_toBinary(_uintIn)); + } + + function _encodeAddress(address _addressIn) internal pure returns (bytes memory addressBytes) { + bytes memory addressRaw = new bytes(20); + assembly { + mstore(add(addressRaw, 0x20), shl(96, _addressIn)) // store address left-aligned + } + addressBytes = _encodeBytes(addressRaw); + } + + function _encodeString(string memory _stringIn) internal pure returns (bytes memory stringBytes) { + stringBytes = _encodeBytes(bytes(_stringIn)); + } + + function _encodeBool(bool _boolIn) internal pure returns (bytes memory boolBytes) { + boolBytes = new bytes(1); + boolBytes[0] = (_boolIn ? bytes1(0x01) : bytes1(0x80)); + } + + function _encodeList(bytes[] memory _bytesToEncode) internal pure returns (bytes memory listBytes) { + listBytes = _flatten(_bytesToEncode); + listBytes = abi.encodePacked(_writeLength(listBytes.length, 192), listBytes); + } + + /// @notice Encode integer in big endian binary form with no leading zeroes. + /// @param _uintValue The integer to encode. + /// @return binaryBytes RLP encoded bytes. + function _toBinary(uint256 _uintValue) private pure returns (bytes memory binaryBytes) { + assembly { + let ptr := mload(0x40) // Get free memory pointer + + let i := 0 + // Scan for first non-zero byte from MSB (big-endian) + for {} lt(i, 32) { + i := add(i, 1) + } { + if iszero(and(shr(sub(248, mul(i, 8)), _uintValue), 0xff)) { + continue + } + break + } + + let length := sub(32, i) // Number of non-zero bytes + binaryBytes := ptr + mstore(binaryBytes, length) + + // Write stripped bytes + for { + let j := 0 + } lt(j, length) { + j := add(j, 1) + } { + let shift := mul(sub(length, add(j, 1)), 8) + let b := and(shr(shift, _uintValue), 0xff) + mstore8(add(add(binaryBytes, 0x20), j), b) + } + + // Move free memory pointer + mstore(0x40, add(add(ptr, 0x20), length)) + } + } + + function _writeLength(uint256 _itemLength, uint256 _offset) private pure returns (bytes memory lengthBytes) { + assembly { + // Start from free memory pointer + lengthBytes := mload(0x40) + + switch lt(_itemLength, 56) + case 1 { + // Case: short length + mstore8(add(lengthBytes, 0x20), add(_itemLength, _offset)) + mstore(lengthBytes, 1) // Set bytes length to 1 + mstore(0x40, add(lengthBytes, 0x21)) // Advance free memory pointer + } + default { + // Case: long length + let temp := _itemLength + let lengthOfLength := 0 + + for {} gt(temp, 0) {} { + lengthOfLength := add(lengthOfLength, 1) + temp := shr(8, temp) + } + + // First byte: offset + 55 + lengthOfLength + mstore8(add(lengthBytes, 0x20), add(add(lengthOfLength, _offset), 55)) + + // Write big-endian bytes of _itemLength + for { + let i := 0 + } lt(i, lengthOfLength) { + i := add(i, 1) + } { + let shift := mul(8, sub(lengthOfLength, add(i, 1))) + let b := and(shr(shift, _itemLength), 0xff) + mstore8(add(add(lengthBytes, 0x21), i), b) + } + + let totalLen := add(lengthOfLength, 1) + mstore(lengthBytes, totalLen) // Set bytes length + mstore(0x40, add(add(lengthBytes, 0x20), totalLen)) // Advance free memory pointer + } + } + } + + // @custom:attribution https://github.com/sammayo/solidity-rlp-encoder + /// @notice Flattens a list of byte strings into one byte string. + /// @dev mcopy is used for the Cancun EVM fork. See original for other forks. + /// @param _bytesList List of byte strings to flatten. + /// @return flattenedBytes The flattened byte string. + function _flatten(bytes[] memory _bytesList) private pure returns (bytes memory flattenedBytes) { + uint256 bytesListLength = _bytesList.length; + if (bytesListLength == 0) { + return new bytes(0); + } + + uint256 flattenedBytesLength; + uint256 reusableCounter; + for (; reusableCounter < bytesListLength; reusableCounter++) { + unchecked { + flattenedBytesLength += _bytesList[reusableCounter].length; + } + } + + flattenedBytes = new bytes(flattenedBytesLength); + + uint256 flattenedPtr; + assembly { + flattenedPtr := add(flattenedBytes, 0x20) + } + + bytes memory item; + uint256 itemLength; + + for (reusableCounter = 0; reusableCounter < bytesListLength; reusableCounter++) { + item = _bytesList[reusableCounter]; + itemLength = item.length; + assembly { + mcopy(flattenedPtr, add(item, 0x20), itemLength) + flattenedPtr := add(flattenedPtr, itemLength) + } + } + } +} diff --git a/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol b/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol index 450d4b851..c1eebfdcc 100644 --- a/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol +++ b/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol @@ -49,12 +49,7 @@ abstract contract L1MessageServiceV1 is * @param _to The recipient of the message and gas refund. * @param _calldata The calldata of the message. */ - modifier distributeFees( - uint256 _feeInWei, - address _to, - bytes calldata _calldata, - address _feeRecipient - ) { + modifier distributeFees(uint256 _feeInWei, address _to, bytes calldata _calldata, address _feeRecipient) { //pre-execution uint256 startingGas = gasleft(); _; diff --git a/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol b/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol index bcd6dec98..584631591 100644 --- a/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol +++ b/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol @@ -173,12 +173,7 @@ abstract contract L2MessageServiceV1 is * @param _to The recipient of the message and gas refund. * @param _calldata The calldata of the message. */ - modifier distributeFees( - uint256 _feeInWei, - address _to, - bytes calldata _calldata, - address _feeRecipient - ) { + modifier distributeFees(uint256 _feeInWei, address _to, bytes calldata _calldata, address _feeRecipient) { //pre-execution uint256 startingGas = gasleft(); _; diff --git a/contracts/src/rollup/LineaRollup.sol b/contracts/src/rollup/LineaRollup.sol index 737ec0ffb..17f5c5eb7 100644 --- a/contracts/src/rollup/LineaRollup.sol +++ b/contracts/src/rollup/LineaRollup.sol @@ -602,9 +602,7 @@ contract LineaRollup is AccessControlUpgradeable, ZkEvmV2, L1MessageService, Per assembly { for { let i := _data.length - } gt(i, 0) { - - } { + } gt(i, 0) {} { i := sub(i, 0x20) let chunk := calldataload(add(_data.offset, i)) if iszero(iszero(and(chunk, 0xFF00000000000000000000000000000000000000000000000000000000000000))) { diff --git a/contracts/src/security/pausing/PauseManager.sol b/contracts/src/security/pausing/PauseManager.sol index 9e9d7d64b..150dd4230 100644 --- a/contracts/src/security/pausing/PauseManager.sol +++ b/contracts/src/security/pausing/PauseManager.sol @@ -150,7 +150,7 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { if (isPaused(_pauseType)) { revert IsPaused(_pauseType); } - + unchecked { if (hasRole(SECURITY_COUNCIL_ROLE, _msgSender())) { pauseExpiryTimestamp = type(uint256).max - COOLDOWN_DURATION; @@ -193,9 +193,7 @@ abstract contract PauseManager is IPauseManager, AccessControlUpgradeable { * @dev Throws if UNUSED pause type is used, or the pause expiry period has not passed. * @param _pauseType The pause type value. */ - function unPauseByExpiredType( - PauseType _pauseType - ) external onlyUsedPausedTypes(_pauseType) { + function unPauseByExpiredType(PauseType _pauseType) external onlyUsedPausedTypes(_pauseType) { if (!isPaused(_pauseType)) { revert IsNotPaused(_pauseType); } diff --git a/contracts/src/verifiers/PlonkVerifierDev.sol b/contracts/src/verifiers/PlonkVerifierDev.sol index 9a1845433..828db1f22 100644 --- a/contracts/src/verifiers/PlonkVerifierDev.sol +++ b/contracts/src/verifiers/PlonkVerifierDev.sol @@ -1323,9 +1323,7 @@ contract PlonkVerifierDev { mstore(add(mPtr, 0x80), e) mstore(add(mPtr, 0xa0), R_MOD) let check_staticcall := staticcall(gas(), MOD_EXP, mPtr, 0xc0, mPtr, 0x20) - if eq(check_staticcall, 0) { - - } + if eq(check_staticcall, 0) {} res := mload(mPtr) } } diff --git a/contracts/test/foundry/LineaRollup.t.sol b/contracts/test/foundry/LineaRollup.t.sol index de7619022..57b4e3a9a 100644 --- a/contracts/test/foundry/LineaRollup.t.sol +++ b/contracts/test/foundry/LineaRollup.t.sol @@ -10,7 +10,7 @@ import { ILineaRollup } from "src/rollup/interfaces/ILineaRollup.sol"; import { IPauseManager } from "src/security/pausing/interfaces/IPauseManager.sol"; import { IPermissionsManager } from "src/security/access/interfaces/IPermissionsManager.sol"; import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import { ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract LineaRollupTestHelper is LineaRollup { function calculateY(bytes calldata data, bytes32 dataEvaluationPoint) external pure returns (bytes32) { @@ -24,7 +24,7 @@ contract LineaRollupTestHelper is LineaRollup { bytes32 _dataEvaluationPoint, bytes32 _dataEvaluationClaim ) external pure returns (bytes32 shnarf) { - return _computeShnarf(_parentShnarf, _snarkHash, _finalStateRootHash, _dataEvaluationPoint, _dataEvaluationClaim); + return _computeShnarf(_parentShnarf, _snarkHash, _finalStateRootHash, _dataEvaluationPoint, _dataEvaluationClaim); } } @@ -95,17 +95,14 @@ contract LineaRollupTest is Test { // Adjust compressedData to start with 0x00 submission.compressedData = hex"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; - bytes32 dataEvaluationPoint = EfficientLeftRightKeccak._efficientKeccak(submission.snarkHash, keccak256(submission.compressedData)); + bytes32 dataEvaluationPoint = EfficientLeftRightKeccak._efficientKeccak( + submission.snarkHash, + keccak256(submission.compressedData) + ); bytes32 dataEvaluationClaim = lineaRollup.calculateY(submission.compressedData, dataEvaluationPoint); - bytes32 parentShnarf = lineaRollup.computeShnarf( - 0x0, - 0x0, - 0x0, - 0x0, - 0x0 - ); + bytes32 parentShnarf = lineaRollup.computeShnarf(0x0, 0x0, 0x0, 0x0, 0x0); bytes32 expectedShnarf = keccak256( abi.encodePacked( diff --git a/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldata.json b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldata.json new file mode 100644 index 000000000..8f1144d53 --- /dev/null +++ b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldata.json @@ -0,0 +1,27 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockHash": "0xeb8235afc4b60d415a9567f36c0c5a5e4662f0b8c93f5343beb61af105dba6c2", + "blockNumber": "0x151b18c", + "from": "0x13fb49bd78e18fff091c0b9019f052f1dd6d7433", + "gas": "0x18fd1", + "gasPrice": "0x2b1c92c6", + "maxFeePerGas": "0x35a4e900", + "maxPriorityFeePerGas": "0x2faf080", + "hash": "0xd147a5f40f4224c0ed73777ba74f590c683ab0a4c8553b195d84bc5dfbd3ae0b", + "input": "0x9f3ce55a00000000000000000000000013fb49bd78e18fff091c0b9019f052f1dd6d743300000000000000000000000000000000000000000000000000000a400e08caa400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x8b", + "to": "0xd19d4b5d358258f05d7b411e21a1460d11b0876f", + "transactionIndex": "0xaa", + "value": "0x166dd3710594aa4", + "type": "0x2", + "accessList": [], + "chainId": "0x1", + "v": "0x1", + "r": "0x1fdb8dea465b1fa7c8e77b09a26cbfe1aa2b9b5110a78e39c3414ad516243621", + "s": "0x716097a1f1e0d8489b9e9cdd91dca9e6207832e26b350b4146dacec914b238dd", + "yParity": "0x1" + }, + "rlpEncoded": "0x02f8f901818b8402faf0808435a4e90083018fd194d19d4b5d358258f05d7b411e21a1460d11b0876f880166dd3710594aa4b8849f3ce55a00000000000000000000000013fb49bd78e18fff091c0b9019f052f1dd6d743300000000000000000000000000000000000000000000000000000a400e08caa400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000c001a01fdb8dea465b1fa7c8e77b09a26cbfe1aa2b9b5110a78e39c3414ad516243621a0716097a1f1e0d8489b9e9cdd91dca9e6207832e26b350b4146dacec914b238dd" +} \ No newline at end of file diff --git a/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withoutCalldata.json b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withoutCalldata.json new file mode 100644 index 000000000..831189bf1 --- /dev/null +++ b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withoutCalldata.json @@ -0,0 +1,27 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "accessList": [], + "blockHash": "0xd20ab77f23688dbd42e3c8fa2d448c070bb4dc9ce5cae8f393e99907fde41755", + "blockNumber": "0x151b88e", + "chainId": "0x1", + "from": "0x0d85129b29781bab8a422b1bcd60b68f45dd2c40", + "gas": "0x5208", + "gasPrice": "0x1fcf0b58", + "hash": "0xd4d9a0482cf4f43d2f712ee6edcf7f6824aed99b5a339b1fae9d093e59c2b394", + "input": "0x", + "maxFeePerGas": "0x2835231b", + "maxPriorityFeePerGas": "0xe195f", + "nonce": "0x87", + "r": "0x266af86d7a0de8ad3cad80493401207ac2c7470fd66be304a2efbb33fbabb8a5", + "s": "0x7e15a5b7a18360eaebfc2635f84abecfbce441037afcacbc39f46af810e9fa5d", + "to": "0xdfaa75323fb721e5f29d43859390f62cc4b600b8", + "transactionIndex": "0x61", + "type": "0x2", + "v": "0x0", + "value": "0x20e0e27022b718", + "yParity": "0x0" + }, + "rlpEncoded": "0x02f871018187830e195f842835231b82520894dfaa75323fb721e5f29d43859390f62cc4b600b88720e0e27022b71880c080a0266af86d7a0de8ad3cad80493401207ac2c7470fd66be304a2efbb33fbabb8a5a07e15a5b7a18360eaebfc2635f84abecfbce441037afcacbc39f46af810e9fa5d" +} \ No newline at end of file diff --git a/contracts/test/hardhat/common/helpers/hashing.ts b/contracts/test/hardhat/common/helpers/hashing.ts index 7c8815cca..6de667c5c 100644 --- a/contracts/test/hardhat/common/helpers/hashing.ts +++ b/contracts/test/hardhat/common/helpers/hashing.ts @@ -1,6 +1,8 @@ -import { ethers } from "ethers"; +import { BytesLike, ethers } from "ethers"; import { encodeData } from "./encoding"; +export const generateKeccak256BytesDirectly = (data: BytesLike) => ethers.keccak256(data); + export const generateKeccak256Hash = (str: string) => generateKeccak256(["string"], [str], true); export const generateKeccak256 = (types: string[], values: unknown[], packed?: boolean) => diff --git a/contracts/test/hardhat/common/types.ts b/contracts/test/hardhat/common/types.ts index a33a30abd..b79affcbd 100644 --- a/contracts/test/hardhat/common/types.ts +++ b/contracts/test/hardhat/common/types.ts @@ -84,3 +84,16 @@ export type FinalizationData = { }; export type ShnarfDataGenerator = (blobParentShnarfIndex: number, isMultiple?: boolean) => ShnarfData; + +export type Eip1559Transaction = { + nonce: bigint; + maxPriorityFeePerGas: bigint; + maxFeePerGas: bigint; + gasLimit: bigint; + to: string; + value: bigint; + input: string; + v: bigint; + r: bigint; + s: bigint; +}; diff --git a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts new file mode 100644 index 000000000..72e92c4f1 --- /dev/null +++ b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts @@ -0,0 +1,57 @@ +import { expect } from "chai"; +import { TestEip1559RlpEncoder } from "../../../typechain-types"; +import { deployFromFactory } from "../common/deployment"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import transactionWithoutCalldata from "../_testData/eip1559RlpEncoderTransactions/withoutCalldata.json"; +import transactionWithCalldata from "../_testData/eip1559RlpEncoderTransactions/withCalldata.json"; +import { Eip1559Transaction } from "../common/types"; +import { generateKeccak256BytesDirectly } from "../common/helpers"; + +describe.only("Eip1559RlpEncoder Library", () => { + let contract: TestEip1559RlpEncoder; + + async function deployTestEip1559RlpEncoderFixture() { + return deployFromFactory("TestEip1559RlpEncoder", 1); + } + beforeEach(async () => { + contract = (await loadFixture(deployTestEip1559RlpEncoderFixture)) as TestEip1559RlpEncoder; + }); + + describe("RLP Encoding and hashing", () => { + it("Succeeds for a transaction without calldata", async () => { + const { rlpEncodedTransaction, transactionHash } = await contract.encodeEip1559Transaction( + buildEip1559Transaction(transactionWithoutCalldata.result), + ); + + expect(transactionWithoutCalldata.result.hash).equal(transactionHash); + expect(transactionWithoutCalldata.rlpEncoded).equal(rlpEncodedTransaction); + expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal(transactionWithoutCalldata.result.hash); + }); + + it("Succeeds for a transaction with calldata", async () => { + const { rlpEncodedTransaction, transactionHash } = await contract.encodeEip1559Transaction( + buildEip1559Transaction(transactionWithCalldata.result), + ); + + expect(transactionWithCalldata.result.hash).equal(transactionHash); + expect(transactionWithCalldata.rlpEncoded).equal(rlpEncodedTransaction); + expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal(transactionWithCalldata.result.hash); + }); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function buildEip1559Transaction(data: any): Eip1559Transaction { + return { + nonce: data.nonce, + maxPriorityFeePerGas: data.maxPriorityFeePerGas, + maxFeePerGas: data.maxFeePerGas, + gasLimit: data.gas, + to: data.to, + value: data.value, + input: data.input, + v: data.v, + r: data.r, + s: data.s, + }; + } +}); From 0f92c40231f7505aad90909e5b6baf3d5e459a27 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Thu, 27 Mar 2025 17:53:46 +0000 Subject: [PATCH 02/16] remove restriction --- contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts index 72e92c4f1..47718ddd4 100644 --- a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts +++ b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts @@ -7,7 +7,7 @@ import transactionWithCalldata from "../_testData/eip1559RlpEncoderTransactions/ import { Eip1559Transaction } from "../common/types"; import { generateKeccak256BytesDirectly } from "../common/helpers"; -describe.only("Eip1559RlpEncoder Library", () => { +describe("Eip1559RlpEncoder Library", () => { let contract: TestEip1559RlpEncoder; async function deployTestEip1559RlpEncoderFixture() { From 525a2bb5612ab3d12b173967874f399bd7649fa5 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Thu, 27 Mar 2025 17:57:52 +0000 Subject: [PATCH 03/16] reset with pnpm i --- contracts/src/libraries/RlpWriter.sol | 10 ++++++++-- contracts/src/messaging/l1/v1/L1MessageServiceV1.sol | 7 ++++++- contracts/src/messaging/l2/v1/L2MessageServiceV1.sol | 7 ++++++- contracts/src/rollup/LineaRollup.sol | 4 +++- contracts/src/verifiers/PlonkVerifierDev.sol | 4 +++- pnpm-lock.yaml | 2 -- 6 files changed, 26 insertions(+), 8 deletions(-) diff --git a/contracts/src/libraries/RlpWriter.sol b/contracts/src/libraries/RlpWriter.sol index 5f4c4dc3c..9b91b9d55 100644 --- a/contracts/src/libraries/RlpWriter.sol +++ b/contracts/src/libraries/RlpWriter.sol @@ -78,7 +78,9 @@ library RlpWriter { let i := 0 // Scan for first non-zero byte from MSB (big-endian) - for {} lt(i, 32) { + for { + + } lt(i, 32) { i := add(i, 1) } { if iszero(and(shr(sub(248, mul(i, 8)), _uintValue), 0xff)) { @@ -124,7 +126,11 @@ library RlpWriter { let temp := _itemLength let lengthOfLength := 0 - for {} gt(temp, 0) {} { + for { + + } gt(temp, 0) { + + } { lengthOfLength := add(lengthOfLength, 1) temp := shr(8, temp) } diff --git a/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol b/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol index c1eebfdcc..450d4b851 100644 --- a/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol +++ b/contracts/src/messaging/l1/v1/L1MessageServiceV1.sol @@ -49,7 +49,12 @@ abstract contract L1MessageServiceV1 is * @param _to The recipient of the message and gas refund. * @param _calldata The calldata of the message. */ - modifier distributeFees(uint256 _feeInWei, address _to, bytes calldata _calldata, address _feeRecipient) { + modifier distributeFees( + uint256 _feeInWei, + address _to, + bytes calldata _calldata, + address _feeRecipient + ) { //pre-execution uint256 startingGas = gasleft(); _; diff --git a/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol b/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol index 584631591..bcd6dec98 100644 --- a/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol +++ b/contracts/src/messaging/l2/v1/L2MessageServiceV1.sol @@ -173,7 +173,12 @@ abstract contract L2MessageServiceV1 is * @param _to The recipient of the message and gas refund. * @param _calldata The calldata of the message. */ - modifier distributeFees(uint256 _feeInWei, address _to, bytes calldata _calldata, address _feeRecipient) { + modifier distributeFees( + uint256 _feeInWei, + address _to, + bytes calldata _calldata, + address _feeRecipient + ) { //pre-execution uint256 startingGas = gasleft(); _; diff --git a/contracts/src/rollup/LineaRollup.sol b/contracts/src/rollup/LineaRollup.sol index 17f5c5eb7..737ec0ffb 100644 --- a/contracts/src/rollup/LineaRollup.sol +++ b/contracts/src/rollup/LineaRollup.sol @@ -602,7 +602,9 @@ contract LineaRollup is AccessControlUpgradeable, ZkEvmV2, L1MessageService, Per assembly { for { let i := _data.length - } gt(i, 0) {} { + } gt(i, 0) { + + } { i := sub(i, 0x20) let chunk := calldataload(add(_data.offset, i)) if iszero(iszero(and(chunk, 0xFF00000000000000000000000000000000000000000000000000000000000000))) { diff --git a/contracts/src/verifiers/PlonkVerifierDev.sol b/contracts/src/verifiers/PlonkVerifierDev.sol index 828db1f22..9a1845433 100644 --- a/contracts/src/verifiers/PlonkVerifierDev.sol +++ b/contracts/src/verifiers/PlonkVerifierDev.sol @@ -1323,7 +1323,9 @@ contract PlonkVerifierDev { mstore(add(mPtr, 0x80), e) mstore(add(mPtr, 0xa0), R_MOD) let check_staticcall := staticcall(gas(), MOD_EXP, mPtr, 0xc0, mPtr, 0x20) - if eq(check_staticcall, 0) {} + if eq(check_staticcall, 0) { + + } res := mload(mPtr) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab34be53e..cf3c1a655 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,8 +248,6 @@ importers: specifier: 17.7.2 version: 17.7.2 - contracts/lib/forge-std: {} - e2e: devDependencies: '@jest/globals': From 6246a460e59bba0f3e2bc6c0f675b3211b5d00bd Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 28 Mar 2025 10:21:37 +0000 Subject: [PATCH 04/16] use better variable name --- contracts/src/libraries/RlpWriter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/libraries/RlpWriter.sol b/contracts/src/libraries/RlpWriter.sol index 9b91b9d55..c792d501c 100644 --- a/contracts/src/libraries/RlpWriter.sol +++ b/contracts/src/libraries/RlpWriter.sol @@ -100,8 +100,8 @@ library RlpWriter { j := add(j, 1) } { let shift := mul(sub(length, add(j, 1)), 8) - let b := and(shr(shift, _uintValue), 0xff) - mstore8(add(add(binaryBytes, 0x20), j), b) + let byteToAdd := and(shr(shift, _uintValue), 0xff) + mstore8(add(add(binaryBytes, 0x20), j), byteToAdd) } // Move free memory pointer From 20f1e673676bc27929a7e33e90aec3a13da21511 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 28 Mar 2025 11:20:16 +0000 Subject: [PATCH 05/16] rename and add NatSpec --- contracts/src/libraries/Eip1559RlpEncoder.sol | 59 +++-- contracts/src/libraries/RlpEncoder.sol | 222 ++++++++++++++++++ contracts/src/libraries/RlpWriter.sol | 197 ---------------- contracts/test/hardhat/common/types.ts | 2 +- .../hardhat/libraries/Eip1559RlpEncoder.ts | 2 +- 5 files changed, 264 insertions(+), 218 deletions(-) create mode 100644 contracts/src/libraries/RlpEncoder.sol delete mode 100644 contracts/src/libraries/RlpWriter.sol diff --git a/contracts/src/libraries/Eip1559RlpEncoder.sol b/contracts/src/libraries/Eip1559RlpEncoder.sol index 045585cf5..69561c542 100644 --- a/contracts/src/libraries/Eip1559RlpEncoder.sol +++ b/contracts/src/libraries/Eip1559RlpEncoder.sol @@ -7,10 +7,27 @@ */ pragma solidity ^0.8.28; -import { RlpWriter } from "./RlpWriter.sol"; +import { RlpEncoder } from "./RlpEncoder.sol"; +/** + * @title Library for RLP Encoding a type 2 EIP-1559 transactions. + * @author ConsenSys Software Inc. + * @custom:security-contact security-report@linea.build + */ library Eip1559RlpEncoder { - /// @dev chainId is defined in the function and access list is not encoded. + /** + * @notice Supporting data for encoding an EIP-1559 transaction. + * @dev NB: ChainId is not on the struct to allow flexibility by the consuming contract. + * @dev NB: Access lists is assumed empty and does not appear here. + * @dev nonce The sender's nonce. + * @dev maxPriorityFeePerGas The max priority fee the user will pay. + * @dev maxFeePerGas The max fee per gas the user will pay. + * @dev gasLimit The limit of gas that the user is prepared to spend. + * @dev input The calldata input for the transaction. + * @dev yParity The yParity in the signature. + * @dev r The r in the signature. + * @dev s The s in the signature. + */ struct Eip1559Transaction { uint256 nonce; uint256 maxPriorityFeePerGas; @@ -19,34 +36,38 @@ library Eip1559RlpEncoder { address to; uint256 value; bytes input; - uint8 v; + uint8 yParity; uint256 r; uint256 s; } + /** + * @notice Internal function that encodes bytes correctly with length data. + * @param _chainId The chainId to encode with. + * @param _transaction The EIP-1559 transaction excluding chainId. + * @return rlpEncodedTransaction The RLP encoded transaction for submitting. + * @return transactionHash The expected transaction hash. + */ function encodeEIP1559Tx( uint256 _chainId, Eip1559Transaction memory _transaction ) internal pure returns (bytes memory rlpEncodedTransaction, bytes32 transactionHash) { bytes[] memory fields = new bytes[](12); - fields[0] = RlpWriter._encodeUint(_chainId); - fields[1] = RlpWriter._encodeUint(_transaction.nonce); - fields[2] = RlpWriter._encodeUint(_transaction.maxPriorityFeePerGas); - fields[3] = RlpWriter._encodeUint(_transaction.maxFeePerGas); - fields[4] = RlpWriter._encodeUint(_transaction.gasLimit); - fields[5] = RlpWriter._encodeAddress(_transaction.to); - fields[6] = RlpWriter._encodeUint(_transaction.value); - fields[7] = RlpWriter._encodeBytes(_transaction.input); - fields[8] = RlpWriter._encodeList(new bytes[](0)); // AccessList empty on purpose. - fields[9] = RlpWriter._encodeUint(_transaction.v); - fields[10] = RlpWriter._encodeUint(_transaction.r); - fields[11] = RlpWriter._encodeUint(_transaction.s); - - bytes memory encodedList = RlpWriter._encodeList(fields); + fields[0] = RlpEncoder._encodeUint(_chainId); + fields[1] = RlpEncoder._encodeUint(_transaction.nonce); + fields[2] = RlpEncoder._encodeUint(_transaction.maxPriorityFeePerGas); + fields[3] = RlpEncoder._encodeUint(_transaction.maxFeePerGas); + fields[4] = RlpEncoder._encodeUint(_transaction.gasLimit); + fields[5] = RlpEncoder._encodeAddress(_transaction.to); + fields[6] = RlpEncoder._encodeUint(_transaction.value); + fields[7] = RlpEncoder._encodeBytes(_transaction.input); + fields[8] = RlpEncoder._encodeList(new bytes[](0)); // AccessList empty on purpose. + fields[9] = RlpEncoder._encodeUint(_transaction.yParity); + fields[10] = RlpEncoder._encodeUint(_transaction.r); + fields[11] = RlpEncoder._encodeUint(_transaction.s); - // Prepend type byte 0x02 - rlpEncodedTransaction = abi.encodePacked(hex"02", encodedList); + rlpEncodedTransaction = abi.encodePacked(hex"02", RlpEncoder._encodeList(fields)); transactionHash = keccak256(rlpEncodedTransaction); } } diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol new file mode 100644 index 000000000..fc67cd4ec --- /dev/null +++ b/contracts/src/libraries/RlpEncoder.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.28; + +/** + * @title Library for RLP Encoding data. + * @author ConsenSys Software Inc. + * @custom:security-contact security-report@linea.build + * @custom:attribution https://github.com/bakaoh/solidity-rlp-encode + * @dev The internals have been significantly modified from the original for readability and gas. + */ +library RlpEncoder { + /** + * @notice Internal function that encodes bytes correctly with length data. + * @param _bytesIn The bytes to be encoded. + * @return encodedBytes The bytes RLP encoded. + */ + function _encodeBytes(bytes memory _bytesIn) internal pure returns (bytes memory encodedBytes) { + if (_bytesIn.length == 1 && uint8(_bytesIn[0]) < 0x80) { + return _bytesIn; + } + + bytes memory lengthPrefix = _encodeLength(_bytesIn.length, 128); + uint256 prefixLen = lengthPrefix.length; + uint256 dataLen = _bytesIn.length; + uint256 totalLen = prefixLen + dataLen; + + assembly { + encodedBytes := mload(0x40) + mstore(encodedBytes, totalLen) + + let dest := add(encodedBytes, 0x20) + let src := add(lengthPrefix, 0x20) + mcopy(dest, src, prefixLen) + + dest := add(dest, prefixLen) + src := add(_bytesIn, 0x20) + mcopy(dest, src, dataLen) + + mstore(0x40, add(add(encodedBytes, 0x20), totalLen)) + } + } + + /** + * @notice Internal function that encodes a uint value as bytes. + * @param _uintIn The uint to be encoded. + * @return encodedBytes The uint encoded as bytes. + */ + function _encodeUint(uint256 _uintIn) internal pure returns (bytes memory encodedBytes) { + encodedBytes = _encodeBytes(_toBinary(_uintIn)); + } + + /** + * @notice Internal function that encodes a address value as bytes. + * @param _addressIn The address to be encoded. + * @return encodedBytes The address encoded as bytes. + */ + function _encodeAddress(address _addressIn) internal pure returns (bytes memory encodedBytes) { + bytes memory addressRaw = new bytes(20); + assembly { + mstore(add(addressRaw, 0x20), shl(96, _addressIn)) + } + encodedBytes = _encodeBytes(addressRaw); + } + + /** + * @notice Internal function that encodes a string value as bytes. + * @param _stringIn The string to be encoded. + * @return encodedBytes The string encoded as bytes. + */ + function _encodeString(string memory _stringIn) internal pure returns (bytes memory encodedBytes) { + encodedBytes = _encodeBytes(bytes(_stringIn)); + } + + /** + * @notice Internal function that encodes a bool value as bytes. + * @param _boolIn The bool to be encoded. + * @return encodedBytes The bool encoded as bytes. + */ + function _encodeBool(bool _boolIn) internal pure returns (bytes memory encodedBytes) { + encodedBytes = new bytes(1); + encodedBytes[0] = (_boolIn ? bytes1(0x01) : bytes1(0x80)); + } + + /** + * @notice Internal function that flattens a bytes array and encodes it. + * @param _bytesToEncode The bytes array to be encoded. + * @return encodedBytes The bytes array encoded as bytes. + */ + function _encodeList(bytes[] memory _bytesToEncode) internal pure returns (bytes memory encodedBytes) { + encodedBytes = _flatten(_bytesToEncode); + encodedBytes = abi.encodePacked(_encodeLength(encodedBytes.length, 192), encodedBytes); + } + + /** + * @notice Private function that encodes an integer in big endian binary form with no leading zeroes. + * @param _uintValue The uint value to be encoded. + * @return encodedBytes The encoded uint. + */ + function _toBinary(uint256 _uintValue) private pure returns (bytes memory encodedBytes) { + assembly { + let ptr := mload(0x40) + + let i := 0 + for { + + } lt(i, 32) { + i := add(i, 1) + } { + if iszero(and(shr(sub(248, mul(i, 8)), _uintValue), 0xff)) { + continue + } + break + } + + let length := sub(32, i) + encodedBytes := ptr + mstore(encodedBytes, length) + + for { + let j := 0 + } lt(j, length) { + j := add(j, 1) + } { + let shift := mul(sub(length, add(j, 1)), 8) + let byteToAdd := and(shr(shift, _uintValue), 0xff) + mstore8(add(add(encodedBytes, 0x20), j), byteToAdd) + } + + mstore(0x40, add(add(ptr, 0x20), length)) + } + } + + /** + * @notice Private function that encodes length. + * @param _itemLength The length of the item. + * @param _offset The item's offset. + * @return encodedBytes The bytes of the length encoded. + */ + function _encodeLength(uint256 _itemLength, uint256 _offset) private pure returns (bytes memory encodedBytes) { + assembly { + encodedBytes := mload(0x40) + + switch lt(_itemLength, 56) + case 1 { + mstore8(add(encodedBytes, 0x20), add(_itemLength, _offset)) + mstore(encodedBytes, 1) + mstore(0x40, add(encodedBytes, 0x21)) + } + default { + let temp := _itemLength + let lengthOfLength := 0 + + for { + + } gt(temp, 0) { + + } { + lengthOfLength := add(lengthOfLength, 1) + temp := shr(8, temp) + } + + mstore8(add(encodedBytes, 0x20), add(add(lengthOfLength, _offset), 55)) + + for { + let i := 0 + } lt(i, lengthOfLength) { + i := add(i, 1) + } { + let shift := mul(8, sub(lengthOfLength, add(i, 1))) + let b := and(shr(shift, _itemLength), 0xff) + mstore8(add(add(encodedBytes, 0x21), i), b) + } + + let totalLen := add(lengthOfLength, 1) + mstore(encodedBytes, totalLen) + mstore(0x40, add(add(encodedBytes, 0x20), totalLen)) + } + } + } + + /** + * @custom:attribution https://github.com/sammayo/solidity-rlp-encoder + * @notice Flattens a list of byte strings into one byte string. + * @dev mcopy is used for the Cancun EVM fork. See original for other forks. + * @param _bytesList List of byte strings to flatten. + * @return flattenedBytes The flattened byte string. + */ + function _flatten(bytes[] memory _bytesList) private pure returns (bytes memory flattenedBytes) { + uint256 bytesListLength = _bytesList.length; + if (bytesListLength == 0) { + return new bytes(0); + } + + uint256 flattenedBytesLength; + uint256 reusableCounter; + for (; reusableCounter < bytesListLength; reusableCounter++) { + unchecked { + flattenedBytesLength += _bytesList[reusableCounter].length; + } + } + + flattenedBytes = new bytes(flattenedBytesLength); + + uint256 flattenedPtr; + assembly { + flattenedPtr := add(flattenedBytes, 0x20) + } + + bytes memory item; + uint256 itemLength; + + for (reusableCounter = 0; reusableCounter < bytesListLength; reusableCounter++) { + item = _bytesList[reusableCounter]; + itemLength = item.length; + assembly { + mcopy(flattenedPtr, add(item, 0x20), itemLength) + flattenedPtr := add(flattenedPtr, itemLength) + } + } + } +} diff --git a/contracts/src/libraries/RlpWriter.sol b/contracts/src/libraries/RlpWriter.sol deleted file mode 100644 index c792d501c..000000000 --- a/contracts/src/libraries/RlpWriter.sol +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/** - * @title Library for RLP Encoding data. - * @author ConsenSys Software Inc. - * @custom:security-contact security-report@linea.build - */ -pragma solidity ^0.8.28; - -/// @custom:attribution https://github.com/bakaoh/solidity-rlp-encode -/// @title RLPWriter -/// @author RLPWriter is a library for encoding Solidity types to RLP bytes. Adapted from Bakaoh's -/// RLPEncode library (https://github.com/bakaoh/solidity-rlp-encode) with -/// modifications to improve legibility and gas consumption. -library RlpWriter { - function _encodeBytes(bytes memory _bytesIn) internal pure returns (bytes memory bytesOut) { - if (_bytesIn.length == 1 && uint8(_bytesIn[0]) < 0x80) { - return _bytesIn; // Return as-is for single-byte < 0x80 - } - - bytes memory lengthPrefix = _writeLength(_bytesIn.length, 128); - uint256 prefixLen = lengthPrefix.length; - uint256 dataLen = _bytesIn.length; - uint256 totalLen = prefixLen + dataLen; - - assembly { - // Allocate output buffer - bytesOut := mload(0x40) - mstore(bytesOut, totalLen) - - // Copy prefix - let dest := add(bytesOut, 0x20) - let src := add(lengthPrefix, 0x20) - mcopy(dest, src, prefixLen) - - // Copy input bytes - dest := add(dest, prefixLen) - src := add(_bytesIn, 0x20) - mcopy(dest, src, dataLen) - - // Move free memory pointer - mstore(0x40, add(add(bytesOut, 0x20), totalLen)) - } - } - - function _encodeUint(uint256 _uintIn) internal pure returns (bytes memory uintBytes) { - uintBytes = _encodeBytes(_toBinary(_uintIn)); - } - - function _encodeAddress(address _addressIn) internal pure returns (bytes memory addressBytes) { - bytes memory addressRaw = new bytes(20); - assembly { - mstore(add(addressRaw, 0x20), shl(96, _addressIn)) // store address left-aligned - } - addressBytes = _encodeBytes(addressRaw); - } - - function _encodeString(string memory _stringIn) internal pure returns (bytes memory stringBytes) { - stringBytes = _encodeBytes(bytes(_stringIn)); - } - - function _encodeBool(bool _boolIn) internal pure returns (bytes memory boolBytes) { - boolBytes = new bytes(1); - boolBytes[0] = (_boolIn ? bytes1(0x01) : bytes1(0x80)); - } - - function _encodeList(bytes[] memory _bytesToEncode) internal pure returns (bytes memory listBytes) { - listBytes = _flatten(_bytesToEncode); - listBytes = abi.encodePacked(_writeLength(listBytes.length, 192), listBytes); - } - - /// @notice Encode integer in big endian binary form with no leading zeroes. - /// @param _uintValue The integer to encode. - /// @return binaryBytes RLP encoded bytes. - function _toBinary(uint256 _uintValue) private pure returns (bytes memory binaryBytes) { - assembly { - let ptr := mload(0x40) // Get free memory pointer - - let i := 0 - // Scan for first non-zero byte from MSB (big-endian) - for { - - } lt(i, 32) { - i := add(i, 1) - } { - if iszero(and(shr(sub(248, mul(i, 8)), _uintValue), 0xff)) { - continue - } - break - } - - let length := sub(32, i) // Number of non-zero bytes - binaryBytes := ptr - mstore(binaryBytes, length) - - // Write stripped bytes - for { - let j := 0 - } lt(j, length) { - j := add(j, 1) - } { - let shift := mul(sub(length, add(j, 1)), 8) - let byteToAdd := and(shr(shift, _uintValue), 0xff) - mstore8(add(add(binaryBytes, 0x20), j), byteToAdd) - } - - // Move free memory pointer - mstore(0x40, add(add(ptr, 0x20), length)) - } - } - - function _writeLength(uint256 _itemLength, uint256 _offset) private pure returns (bytes memory lengthBytes) { - assembly { - // Start from free memory pointer - lengthBytes := mload(0x40) - - switch lt(_itemLength, 56) - case 1 { - // Case: short length - mstore8(add(lengthBytes, 0x20), add(_itemLength, _offset)) - mstore(lengthBytes, 1) // Set bytes length to 1 - mstore(0x40, add(lengthBytes, 0x21)) // Advance free memory pointer - } - default { - // Case: long length - let temp := _itemLength - let lengthOfLength := 0 - - for { - - } gt(temp, 0) { - - } { - lengthOfLength := add(lengthOfLength, 1) - temp := shr(8, temp) - } - - // First byte: offset + 55 + lengthOfLength - mstore8(add(lengthBytes, 0x20), add(add(lengthOfLength, _offset), 55)) - - // Write big-endian bytes of _itemLength - for { - let i := 0 - } lt(i, lengthOfLength) { - i := add(i, 1) - } { - let shift := mul(8, sub(lengthOfLength, add(i, 1))) - let b := and(shr(shift, _itemLength), 0xff) - mstore8(add(add(lengthBytes, 0x21), i), b) - } - - let totalLen := add(lengthOfLength, 1) - mstore(lengthBytes, totalLen) // Set bytes length - mstore(0x40, add(add(lengthBytes, 0x20), totalLen)) // Advance free memory pointer - } - } - } - - // @custom:attribution https://github.com/sammayo/solidity-rlp-encoder - /// @notice Flattens a list of byte strings into one byte string. - /// @dev mcopy is used for the Cancun EVM fork. See original for other forks. - /// @param _bytesList List of byte strings to flatten. - /// @return flattenedBytes The flattened byte string. - function _flatten(bytes[] memory _bytesList) private pure returns (bytes memory flattenedBytes) { - uint256 bytesListLength = _bytesList.length; - if (bytesListLength == 0) { - return new bytes(0); - } - - uint256 flattenedBytesLength; - uint256 reusableCounter; - for (; reusableCounter < bytesListLength; reusableCounter++) { - unchecked { - flattenedBytesLength += _bytesList[reusableCounter].length; - } - } - - flattenedBytes = new bytes(flattenedBytesLength); - - uint256 flattenedPtr; - assembly { - flattenedPtr := add(flattenedBytes, 0x20) - } - - bytes memory item; - uint256 itemLength; - - for (reusableCounter = 0; reusableCounter < bytesListLength; reusableCounter++) { - item = _bytesList[reusableCounter]; - itemLength = item.length; - assembly { - mcopy(flattenedPtr, add(item, 0x20), itemLength) - flattenedPtr := add(flattenedPtr, itemLength) - } - } - } -} diff --git a/contracts/test/hardhat/common/types.ts b/contracts/test/hardhat/common/types.ts index b79affcbd..394eae7c4 100644 --- a/contracts/test/hardhat/common/types.ts +++ b/contracts/test/hardhat/common/types.ts @@ -93,7 +93,7 @@ export type Eip1559Transaction = { to: string; value: bigint; input: string; - v: bigint; + yParity: bigint; r: bigint; s: bigint; }; diff --git a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts index 47718ddd4..dfcf38aa5 100644 --- a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts +++ b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts @@ -49,7 +49,7 @@ describe("Eip1559RlpEncoder Library", () => { to: data.to, value: data.value, input: data.input, - v: data.v, + yParity: data.yParity, r: data.r, s: data.s, }; From 0700c430aa634ccc5648fbd38a56f1b029b1420d Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 28 Mar 2025 11:55:05 +0000 Subject: [PATCH 06/16] finish test coverage and split helper file --- .../unit/libraries/TestRlpEncoder.sol | 14 +++++ contracts/src/libraries/RlpEncoder.sol | 4 +- .../test/hardhat/common/helpers/index.ts | 1 + .../helpers/typedTransactionBuilding.ts | 17 +++++++ .../hardhat/libraries/Eip1559RlpEncoder.ts | 18 +------ .../test/hardhat/libraries/RlpEncoder.ts | 51 +++++++++++++++++++ 6 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 contracts/src/_testing/unit/libraries/TestRlpEncoder.sol create mode 100644 contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts create mode 100644 contracts/test/hardhat/libraries/RlpEncoder.ts diff --git a/contracts/src/_testing/unit/libraries/TestRlpEncoder.sol b/contracts/src/_testing/unit/libraries/TestRlpEncoder.sol new file mode 100644 index 000000000..b92df6b90 --- /dev/null +++ b/contracts/src/_testing/unit/libraries/TestRlpEncoder.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.28; + +import { RlpEncoder } from "../../../libraries/RlpEncoder.sol"; + +contract TestRlpEncoder { + function encodeBool(bool _boolIn) external pure returns (bytes memory encodedBytes) { + return RlpEncoder._encodeBool(_boolIn); + } + + function encodeString(string memory _stringIn) external pure returns (bytes memory encodedBytes) { + return RlpEncoder._encodeString(_stringIn); + } +} diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index fc67cd4ec..68bdbf701 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -99,7 +99,7 @@ library RlpEncoder { */ function _toBinary(uint256 _uintValue) private pure returns (bytes memory encodedBytes) { assembly { - let ptr := mload(0x40) + let ptr := mload(0x40) let i := 0 for { @@ -113,7 +113,7 @@ library RlpEncoder { break } - let length := sub(32, i) + let length := sub(32, i) encodedBytes := ptr mstore(encodedBytes, length) diff --git a/contracts/test/hardhat/common/helpers/index.ts b/contracts/test/hardhat/common/helpers/index.ts index 20415e13c..6ceaceaa2 100644 --- a/contracts/test/hardhat/common/helpers/index.ts +++ b/contracts/test/hardhat/common/helpers/index.ts @@ -6,3 +6,4 @@ export * from "./dataGeneration"; export * from "./dataLoader"; export * from "./expectations"; export * from "./time"; +export * from "./typedTransactionBuilding"; diff --git a/contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts b/contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts new file mode 100644 index 000000000..e46eba277 --- /dev/null +++ b/contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts @@ -0,0 +1,17 @@ +import { Eip1559Transaction } from "../types"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function buildEip1559Transaction(data: any): Eip1559Transaction { + return { + nonce: data.nonce, + maxPriorityFeePerGas: data.maxPriorityFeePerGas, + maxFeePerGas: data.maxFeePerGas, + gasLimit: data.gas, + to: data.to, + value: data.value, + input: data.input, + yParity: data.yParity, + r: data.r, + s: data.s, + }; +} diff --git a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts index dfcf38aa5..30975287a 100644 --- a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts +++ b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts @@ -4,8 +4,8 @@ import { deployFromFactory } from "../common/deployment"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import transactionWithoutCalldata from "../_testData/eip1559RlpEncoderTransactions/withoutCalldata.json"; import transactionWithCalldata from "../_testData/eip1559RlpEncoderTransactions/withCalldata.json"; -import { Eip1559Transaction } from "../common/types"; import { generateKeccak256BytesDirectly } from "../common/helpers"; +import { buildEip1559Transaction } from "../common/helpers/typedTransactionBuilding"; describe("Eip1559RlpEncoder Library", () => { let contract: TestEip1559RlpEncoder; @@ -38,20 +38,4 @@ describe("Eip1559RlpEncoder Library", () => { expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal(transactionWithCalldata.result.hash); }); }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function buildEip1559Transaction(data: any): Eip1559Transaction { - return { - nonce: data.nonce, - maxPriorityFeePerGas: data.maxPriorityFeePerGas, - maxFeePerGas: data.maxFeePerGas, - gasLimit: data.gas, - to: data.to, - value: data.value, - input: data.input, - yParity: data.yParity, - r: data.r, - s: data.s, - }; - } }); diff --git a/contracts/test/hardhat/libraries/RlpEncoder.ts b/contracts/test/hardhat/libraries/RlpEncoder.ts new file mode 100644 index 000000000..e9fb575da --- /dev/null +++ b/contracts/test/hardhat/libraries/RlpEncoder.ts @@ -0,0 +1,51 @@ +import { expect } from "chai"; +import { TestRlpEncoder } from "../../../typechain-types"; +import { deployFromFactory } from "../common/deployment"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; + +describe("RlpEncoder Library", () => { + let contract: TestRlpEncoder; + + async function deployTestEip1559RlpEncoderFixture() { + return deployFromFactory("TestRlpEncoder"); + } + beforeEach(async () => { + contract = (await loadFixture(deployTestEip1559RlpEncoderFixture)) as TestRlpEncoder; + }); + + describe("RLP Encoding", () => { + describe("Bool Encoding", () => { + it("Encodes false correctly", async () => { + const encoded = await contract.encodeBool(false); + expect(encoded).equal("0x80"); + }); + + it("Encodes true correctly", async () => { + const encoded = await contract.encodeBool(true); + expect(encoded).equal("0x01"); + }); + }); + + describe("String Encoding", () => { + it("Encodes blank string correctly", async () => { + const encoded = await contract.encodeString(""); + expect(encoded).equal("0x80"); + }); + + it("Encodes a short string correctly", async () => { + const encoded = await contract.encodeString("short"); + const expected = "0x8573686f7274"; + expect(encoded).equal(expected); + }); + + it("Encodes a long string correctly", async () => { + const encoded = await contract.encodeString( + "This is a string that is quite long and needs some different encoding", + ); + const expected = + "0xb84554686973206973206120737472696e672074686174206973207175697465206c6f6e6720616e64206e6565647320736f6d6520646966666572656e7420656e636f64696e67"; + expect(encoded).equal(expected); + }); + }); + }); +}); From 3086421f742c144b6076f737d0e6a7218c908af2 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 28 Mar 2025 13:37:47 +0000 Subject: [PATCH 07/16] compute totalLen in assembly 2k gas save --- contracts/src/libraries/RlpEncoder.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index 68bdbf701..589602668 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -23,9 +23,9 @@ library RlpEncoder { bytes memory lengthPrefix = _encodeLength(_bytesIn.length, 128); uint256 prefixLen = lengthPrefix.length; uint256 dataLen = _bytesIn.length; - uint256 totalLen = prefixLen + dataLen; assembly { + let totalLen := add(prefixLen, dataLen) encodedBytes := mload(0x40) mstore(encodedBytes, totalLen) From 065c66db34066f01670babb8be559158bc4a0849 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 28 Mar 2025 13:47:13 +0000 Subject: [PATCH 08/16] reuse memory variable for consistency --- contracts/src/libraries/RlpEncoder.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index 589602668..e0a384018 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -56,11 +56,11 @@ library RlpEncoder { * @return encodedBytes The address encoded as bytes. */ function _encodeAddress(address _addressIn) internal pure returns (bytes memory encodedBytes) { - bytes memory addressRaw = new bytes(20); + encodedBytes = new bytes(20); assembly { - mstore(add(addressRaw, 0x20), shl(96, _addressIn)) + mstore(add(encodedBytes, 0x20), shl(96, _addressIn)) } - encodedBytes = _encodeBytes(addressRaw); + encodedBytes = _encodeBytes(encodedBytes); } /** From f3cf95db1f3118c908771693922d3ee90b82b7e5 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 28 Mar 2025 16:36:21 +0000 Subject: [PATCH 09/16] add larger calldata test --- .../withLargeCalldata.json | 27 +++++++++++++++++++ .../hardhat/libraries/Eip1559RlpEncoder.ts | 11 ++++++++ 2 files changed, 38 insertions(+) create mode 100644 contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withLargeCalldata.json diff --git a/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withLargeCalldata.json b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withLargeCalldata.json new file mode 100644 index 000000000..0b9a41247 --- /dev/null +++ b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withLargeCalldata.json @@ -0,0 +1,27 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "blockHash": "0xa82b9ee49c14ea79ba6983a2cf91f86bce8f32b887bde513576459c7b0299386", + "blockNumber": "0x151b841", + "from": "0x92e12ebea85129568bc2a5566bea367c036946c8", + "gas": "0x7191b", + "gasPrice": "0x2ba2d83d", + "maxFeePerGas": "0x540ae480", + "maxPriorityFeePerGas": "0x4b571c0", + "hash": "0xdd2c0345d1c559797592d1deaf20a04ca1571129bcc0117814e2343cb38d4f0c", + "input": "0x2213bc0b0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000009c41fff991f00000000000000000000000092e12ebea85129568bc2a5566bea367c036946c800000000000000000000000095ad61b0a150d79219dcf64e1e6cc01f0b64c4ce000000000000000000000000000000000000000000000e0784d9c87d94a8117000000000000000000000000000000000000000000000000000000000000000a0e39d71a20266d3a1242141272068d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000000e4c1fb425e0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067e444dd00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164af72634f0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000ffffffffffffffc5000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034271001eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0001f400000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164af72634f0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000ffffffffffffffc500000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003427100195ad61b0a150d79219dcf64e1e6cc01f0b64c4ce0027100000c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c14700000000000000000000000095ad61b0a150d79219dcf64e1e6cc01f0b64c4ce000000000000000000000000000000000000000000000000000000000000005500000000000000000000000095ad61b0a150d79219dcf64e1e6cc01f0b64c4ce000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000007afa9d836d2fccf172b66622625e56404e465dbd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x4", + "to": "0x0000000000001ff3684f28c67538d4d072c22734", + "transactionIndex": "0x35", + "value": "0x0", + "type": "0x2", + "accessList": [], + "chainId": "0x1", + "v": "0x1", + "r": "0x84f9fa394af52883b66c7b65f921aceac201a7b0f7ffbc78fbeea7695a109ad5", + "s": "0x5ca82e86c3174e09e1bfb23b3e91f3d707ed1cd1181cd920c5dc5fbb8ecba914", + "yParity": "0x1" + }, + "rlpEncoded" : "0x02f90b1101048404b571c084540ae4808307191b940000000000001ff3684f28c67538d4d072c2273480b90aa42213bc0b0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000009c41fff991f00000000000000000000000092e12ebea85129568bc2a5566bea367c036946c800000000000000000000000095ad61b0a150d79219dcf64e1e6cc01f0b64c4ce000000000000000000000000000000000000000000000e0784d9c87d94a8117000000000000000000000000000000000000000000000000000000000000000a0e39d71a20266d3a1242141272068d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000000e4c1fb425e0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067e444dd00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164af72634f0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000ffffffffffffffc5000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034271001eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0001f400000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000164af72634f0000000000000000000000000d0e364aa7852291883c162b22d6d81f6355428f000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000ffffffffffffffc500000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003427100195ad61b0a150d79219dcf64e1e6cc01f0b64c4ce0027100000c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c14700000000000000000000000095ad61b0a150d79219dcf64e1e6cc01f0b64c4ce000000000000000000000000000000000000000000000000000000000000005500000000000000000000000095ad61b0a150d79219dcf64e1e6cc01f0b64c4ce000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000007afa9d836d2fccf172b66622625e56404e465dbd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c001a084f9fa394af52883b66c7b65f921aceac201a7b0f7ffbc78fbeea7695a109ad5a05ca82e86c3174e09e1bfb23b3e91f3d707ed1cd1181cd920c5dc5fbb8ecba914" +} \ No newline at end of file diff --git a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts index 30975287a..bc79100f8 100644 --- a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts +++ b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts @@ -4,6 +4,7 @@ import { deployFromFactory } from "../common/deployment"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import transactionWithoutCalldata from "../_testData/eip1559RlpEncoderTransactions/withoutCalldata.json"; import transactionWithCalldata from "../_testData/eip1559RlpEncoderTransactions/withCalldata.json"; +import transactionwithLargeCalldata from "../_testData/eip1559RlpEncoderTransactions/withLargeCalldata.json"; import { generateKeccak256BytesDirectly } from "../common/helpers"; import { buildEip1559Transaction } from "../common/helpers/typedTransactionBuilding"; @@ -37,5 +38,15 @@ describe("Eip1559RlpEncoder Library", () => { expect(transactionWithCalldata.rlpEncoded).equal(rlpEncodedTransaction); expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal(transactionWithCalldata.result.hash); }); + + it("Succeeds for a transaction with large calldata", async () => { + const { rlpEncodedTransaction, transactionHash } = await contract.encodeEip1559Transaction( + buildEip1559Transaction(transactionwithLargeCalldata.result), + ); + + expect(transactionwithLargeCalldata.result.hash).equal(transactionHash); + expect(transactionwithLargeCalldata.rlpEncoded).equal(rlpEncodedTransaction); + expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal(transactionwithLargeCalldata.result.hash); + }); }); }); From 73093792f8ea4315e090cd441f8d17ea346c98ae Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Sat, 29 Mar 2025 15:45:16 +0000 Subject: [PATCH 10/16] support access lists --- contracts/src/libraries/Eip1559RlpEncoder.sol | 4 +- contracts/src/libraries/RlpEncoder.sol | 38 +++++++++++++++++++ .../withCalldataAndAccessList.json | 34 +++++++++++++++++ .../helpers/typedTransactionBuilding.ts | 8 +++- contracts/test/hardhat/common/types.ts | 11 ++++++ .../hardhat/libraries/Eip1559RlpEncoder.ts | 25 +++++++++--- 6 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldataAndAccessList.json diff --git a/contracts/src/libraries/Eip1559RlpEncoder.sol b/contracts/src/libraries/Eip1559RlpEncoder.sol index 69561c542..93f0cd47e 100644 --- a/contracts/src/libraries/Eip1559RlpEncoder.sol +++ b/contracts/src/libraries/Eip1559RlpEncoder.sol @@ -24,6 +24,7 @@ library Eip1559RlpEncoder { * @dev maxFeePerGas The max fee per gas the user will pay. * @dev gasLimit The limit of gas that the user is prepared to spend. * @dev input The calldata input for the transaction. + * @dev accessList The access list used. * @dev yParity The yParity in the signature. * @dev r The r in the signature. * @dev s The s in the signature. @@ -36,6 +37,7 @@ library Eip1559RlpEncoder { address to; uint256 value; bytes input; + RlpEncoder.AccessList[] accessList; uint8 yParity; uint256 r; uint256 s; @@ -62,7 +64,7 @@ library Eip1559RlpEncoder { fields[5] = RlpEncoder._encodeAddress(_transaction.to); fields[6] = RlpEncoder._encodeUint(_transaction.value); fields[7] = RlpEncoder._encodeBytes(_transaction.input); - fields[8] = RlpEncoder._encodeList(new bytes[](0)); // AccessList empty on purpose. + fields[8] = RlpEncoder._encodeAccessList(_transaction.accessList); fields[9] = RlpEncoder._encodeUint(_transaction.yParity); fields[10] = RlpEncoder._encodeUint(_transaction.r); fields[11] = RlpEncoder._encodeUint(_transaction.s); diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index e0a384018..dc583253e 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -10,6 +10,16 @@ pragma solidity ^0.8.28; * @dev The internals have been significantly modified from the original for readability and gas. */ library RlpEncoder { + /** + * @notice Supporting data for encoding an EIP-2930/1559 access lists. + * @dev contractAddress is the address where the storageKeys will be accessed. + * @dev storageKeys contains the list of keys expected to be accessed at contractAddress. + */ + struct AccessList { + address contractAddress; + bytes32[] storageKeys; + } + /** * @notice Internal function that encodes bytes correctly with length data. * @param _bytesIn The bytes to be encoded. @@ -92,6 +102,34 @@ library RlpEncoder { encodedBytes = abi.encodePacked(_encodeLength(encodedBytes.length, 192), encodedBytes); } + /** + * @notice Internal function that encodes an access list as bytes. + * @param _accesslist The access list to be encoded. + * @return encodedBytes The AccessList encoded as bytes. + */ + function _encodeAccessList(AccessList[] memory _accesslist) internal pure returns (bytes memory encodedBytes) { + uint256 listLength = _accesslist.length; + bytes[] memory encodedAccessList = new bytes[](listLength); + + for (uint256 i; i < listLength; ++i) { + bytes32[] memory storageKeys = _accesslist[i].storageKeys; + uint256 keyCount = storageKeys.length; + + bytes[] memory encodedKeys = new bytes[](keyCount); + for (uint256 j; j < keyCount; ++j) { + encodedKeys[j] = _encodeBytes(abi.encodePacked(storageKeys[j])); + } + + bytes[] memory accountTuple = new bytes[](2); + accountTuple[0] = _encodeAddress(_accesslist[i].contractAddress); + accountTuple[1] = _encodeList(encodedKeys); + + encodedAccessList[i] = _encodeList(accountTuple); + } + + encodedBytes = _encodeList(encodedAccessList); + } + /** * @notice Private function that encodes an integer in big endian binary form with no leading zeroes. * @param _uintValue The uint value to be encoded. diff --git a/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldataAndAccessList.json b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldataAndAccessList.json new file mode 100644 index 000000000..aa0e1add8 --- /dev/null +++ b/contracts/test/hardhat/_testData/eip1559RlpEncoderTransactions/withCalldataAndAccessList.json @@ -0,0 +1,34 @@ +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "accessList": [ + { + "address": "0x2926ec066455a9c519a19ac6f641aa40c829d482", + "storageKeys": [ + "0x872009bf950ed1cb0457060a0d6d958528670bae633670177c017a074b0779c3" + ] + } + ], + "blockHash": "0x1c252c8c5bd155c4b3819a61f282d08722e46894875deb7c5c137bd6424bacd9", + "blockNumber": "0x79f253", + "chainId": "0xaa36a7", + "from": "0xfc8769485986fe00760a40747d0d2207bc92c86d", + "gas": "0xc10c", + "gasPrice": "0xcf4f6533", + "hash": "0x59f686557e2f5ce5a774645bb47044b93b3b4cd0d8bc37dba2d244c1d7bdc0af", + "input": "0xf1bca7a400000000000000000000000000000000000000000000000000000000075bcd15", + "maxFeePerGas": "0x1b0a5ce2c", + "maxPriorityFeePerGas": "0xf4240", + "nonce": "0x4", + "r": "0x41b1e39b0b9720e0ada0a9053edfa5e486ec9589bafceb38656d9fa1d6618b8b", + "s": "0x7e15abc0dbd2b96124e70b615142f888576875d9944b2abae284b1d82cd90c21", + "to": "0x5ad9369254f29b724d98f6ce98cb7bad729969f3", + "transactionIndex": "0x60", + "type": "0x2", + "v": "0x1", + "value": "0x0", + "yParity": "0x1" + }, + "rlpEncoded" : "0x02f8ca83aa36a704830f42408501b0a5ce2c82c10c945ad9369254f29b724d98f6ce98cb7bad729969f380a4f1bca7a400000000000000000000000000000000000000000000000000000000075bcd15f838f7942926ec066455a9c519a19ac6f641aa40c829d482e1a0872009bf950ed1cb0457060a0d6d958528670bae633670177c017a074b0779c301a041b1e39b0b9720e0ada0a9053edfa5e486ec9589bafceb38656d9fa1d6618b8ba07e15abc0dbd2b96124e70b615142f888576875d9944b2abae284b1d82cd90c21" +} \ No newline at end of file diff --git a/contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts b/contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts index e46eba277..d8a64e1e6 100644 --- a/contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts +++ b/contracts/test/hardhat/common/helpers/typedTransactionBuilding.ts @@ -1,7 +1,9 @@ -import { Eip1559Transaction } from "../types"; +import { AccessListEntryInput, Eip1559Transaction } from "../types"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function buildEip1559Transaction(data: any): Eip1559Transaction { + const accessList: AccessListEntryInput[] = data.accessList; + return { nonce: data.nonce, maxPriorityFeePerGas: data.maxPriorityFeePerGas, @@ -10,6 +12,10 @@ export function buildEip1559Transaction(data: any): Eip1559Transaction { to: data.to, value: data.value, input: data.input, + accessList: accessList.map(({ address, storageKeys }) => ({ + contractAddress: address, + storageKeys, + })), yParity: data.yParity, r: data.r, s: data.s, diff --git a/contracts/test/hardhat/common/types.ts b/contracts/test/hardhat/common/types.ts index 394eae7c4..eaa1708ec 100644 --- a/contracts/test/hardhat/common/types.ts +++ b/contracts/test/hardhat/common/types.ts @@ -93,7 +93,18 @@ export type Eip1559Transaction = { to: string; value: bigint; input: string; + accessList: AccessList[]; yParity: bigint; r: bigint; s: bigint; }; + +export type AccessList = { + contractAddress: string; + storageKeys: string[]; +}; + +export type AccessListEntryInput = { + address: string; + storageKeys: string[]; +}; diff --git a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts index bc79100f8..b59961037 100644 --- a/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts +++ b/contracts/test/hardhat/libraries/Eip1559RlpEncoder.ts @@ -4,7 +4,8 @@ import { deployFromFactory } from "../common/deployment"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import transactionWithoutCalldata from "../_testData/eip1559RlpEncoderTransactions/withoutCalldata.json"; import transactionWithCalldata from "../_testData/eip1559RlpEncoderTransactions/withCalldata.json"; -import transactionwithLargeCalldata from "../_testData/eip1559RlpEncoderTransactions/withLargeCalldata.json"; +import transactionWithLargeCalldata from "../_testData/eip1559RlpEncoderTransactions/withLargeCalldata.json"; +import transactionWithCalldataAndAccessList from "../_testData/eip1559RlpEncoderTransactions/withCalldataAndAccessList.json"; import { generateKeccak256BytesDirectly } from "../common/helpers"; import { buildEip1559Transaction } from "../common/helpers/typedTransactionBuilding"; @@ -41,12 +42,26 @@ describe("Eip1559RlpEncoder Library", () => { it("Succeeds for a transaction with large calldata", async () => { const { rlpEncodedTransaction, transactionHash } = await contract.encodeEip1559Transaction( - buildEip1559Transaction(transactionwithLargeCalldata.result), + buildEip1559Transaction(transactionWithLargeCalldata.result), ); - expect(transactionwithLargeCalldata.result.hash).equal(transactionHash); - expect(transactionwithLargeCalldata.rlpEncoded).equal(rlpEncodedTransaction); - expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal(transactionwithLargeCalldata.result.hash); + expect(transactionWithLargeCalldata.result.hash).equal(transactionHash); + expect(transactionWithLargeCalldata.rlpEncoded).equal(rlpEncodedTransaction); + expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal(transactionWithLargeCalldata.result.hash); + }); + + it("Succeeds for a transaction with calldata and an access list", async () => { + const sepoliaContract = (await deployFromFactory("TestEip1559RlpEncoder", 11155111)) as TestEip1559RlpEncoder; + + const { rlpEncodedTransaction, transactionHash } = await sepoliaContract.encodeEip1559Transaction( + buildEip1559Transaction(transactionWithCalldataAndAccessList.result), + ); + + expect(transactionWithCalldataAndAccessList.result.hash).equal(transactionHash); + expect(transactionWithCalldataAndAccessList.rlpEncoded).equal(rlpEncodedTransaction); + expect(generateKeccak256BytesDirectly(rlpEncodedTransaction)).equal( + transactionWithCalldataAndAccessList.result.hash, + ); }); }); }); From 49b806ae96d9ef7936671e3fe7f7668468b08563 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Sat, 29 Mar 2025 15:48:31 +0000 Subject: [PATCH 11/16] use standard naming conventions --- contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol | 2 +- contracts/src/libraries/Eip1559RlpEncoder.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol b/contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol index 94b1565d5..33b95e543 100644 --- a/contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol +++ b/contracts/src/_testing/unit/libraries/TestEip1559RlpEncoder.sol @@ -13,6 +13,6 @@ contract TestEip1559RlpEncoder { function encodeEip1559Transaction( Eip1559RlpEncoder.Eip1559Transaction calldata _transaction ) external view returns (bytes memory rlpEncodedTransaction, bytes32 transactionHash) { - (rlpEncodedTransaction, transactionHash) = Eip1559RlpEncoder.encodeEIP1559Tx(chainId, _transaction); + (rlpEncodedTransaction, transactionHash) = Eip1559RlpEncoder._encodeEip1559Transaction(chainId, _transaction); } } diff --git a/contracts/src/libraries/Eip1559RlpEncoder.sol b/contracts/src/libraries/Eip1559RlpEncoder.sol index 93f0cd47e..dddc9e98a 100644 --- a/contracts/src/libraries/Eip1559RlpEncoder.sol +++ b/contracts/src/libraries/Eip1559RlpEncoder.sol @@ -50,7 +50,7 @@ library Eip1559RlpEncoder { * @return rlpEncodedTransaction The RLP encoded transaction for submitting. * @return transactionHash The expected transaction hash. */ - function encodeEIP1559Tx( + function _encodeEip1559Transaction( uint256 _chainId, Eip1559Transaction memory _transaction ) internal pure returns (bytes memory rlpEncodedTransaction, bytes32 transactionHash) { From 93ff8362f4b0df5cbe5a58c20a81a8f691393ae4 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Mon, 31 Mar 2025 09:12:58 +0100 Subject: [PATCH 12/16] let the compile control increments --- contracts/src/libraries/RlpEncoder.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index dc583253e..dd327d7e4 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -111,12 +111,12 @@ library RlpEncoder { uint256 listLength = _accesslist.length; bytes[] memory encodedAccessList = new bytes[](listLength); - for (uint256 i; i < listLength; ++i) { + for (uint256 i; i < listLength; i++) { bytes32[] memory storageKeys = _accesslist[i].storageKeys; uint256 keyCount = storageKeys.length; bytes[] memory encodedKeys = new bytes[](keyCount); - for (uint256 j; j < keyCount; ++j) { + for (uint256 j; j < keyCount; j++) { encodedKeys[j] = _encodeBytes(abi.encodePacked(storageKeys[j])); } From 5f42ca283c396c8c77cb4b6989bc4a0f9d8ea93a Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Mon, 31 Mar 2025 11:47:55 +0100 Subject: [PATCH 13/16] support signed ints --- .../src/_testing/unit/libraries/TestRlpEncoder.sol | 4 ++++ contracts/src/libraries/RlpEncoder.sol | 9 +++++++++ contracts/test/hardhat/libraries/RlpEncoder.ts | 11 +++++++++++ 3 files changed, 24 insertions(+) diff --git a/contracts/src/_testing/unit/libraries/TestRlpEncoder.sol b/contracts/src/_testing/unit/libraries/TestRlpEncoder.sol index b92df6b90..d2a239469 100644 --- a/contracts/src/_testing/unit/libraries/TestRlpEncoder.sol +++ b/contracts/src/_testing/unit/libraries/TestRlpEncoder.sol @@ -11,4 +11,8 @@ contract TestRlpEncoder { function encodeString(string memory _stringIn) external pure returns (bytes memory encodedBytes) { return RlpEncoder._encodeString(_stringIn); } + + function encodeInt(int _intIn) external pure returns (bytes memory encodedBytes) { + return RlpEncoder._encodeInt(_intIn); + } } diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index dd327d7e4..71e4f007c 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -60,6 +60,15 @@ library RlpEncoder { encodedBytes = _encodeBytes(_toBinary(_uintIn)); } + /** + * @notice Internal function that encodes an int value as bytes. + * @param _intIn The int to encode. + * @return encodedBytes The int encoded as bytes. + */ + function _encodeInt(int256 _intIn) internal pure returns (bytes memory encodedBytes) { + encodedBytes = _encodeUint(uint256(_intIn)); + } + /** * @notice Internal function that encodes a address value as bytes. * @param _addressIn The address to be encoded. diff --git a/contracts/test/hardhat/libraries/RlpEncoder.ts b/contracts/test/hardhat/libraries/RlpEncoder.ts index e9fb575da..1383cc192 100644 --- a/contracts/test/hardhat/libraries/RlpEncoder.ts +++ b/contracts/test/hardhat/libraries/RlpEncoder.ts @@ -48,4 +48,15 @@ describe("RlpEncoder Library", () => { }); }); }); + + describe("Int Encoding", () => { + it("Encodes a negative int correctly", async () => { + const encoded = await contract.encodeInt(-123456789n); + expect(encoded).equal("0xa0fffffffffffffffffffffffffffffffffffffffffffffffffffffffff8a432eb"); + }); + it("Encodes a positive int correctly", async () => { + const encoded = await contract.encodeInt(123456789n); + expect(encoded).equal("0x84075bcd15"); + }); + }); }); From 11f13e8c961ef40b78c01f127c7343ac48c90f4d Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 4 Apr 2025 10:41:59 +0100 Subject: [PATCH 14/16] use binary search from kyzooghost --- contracts/src/libraries/RlpEncoder.sol | 31 +++++++++---------- .../test/hardhat/libraries/RlpEncoder.ts | 27 ++++++++++++++++ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index 71e4f007c..7b8699e46 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -26,9 +26,7 @@ library RlpEncoder { * @return encodedBytes The bytes RLP encoded. */ function _encodeBytes(bytes memory _bytesIn) internal pure returns (bytes memory encodedBytes) { - if (_bytesIn.length == 1 && uint8(_bytesIn[0]) < 0x80) { - return _bytesIn; - } + if (_bytesIn.length == 1 && uint8(_bytesIn[0]) < 0x80) return _bytesIn; bytes memory lengthPrefix = _encodeLength(_bytesIn.length, 128); uint256 prefixLen = lengthPrefix.length; @@ -141,37 +139,40 @@ library RlpEncoder { /** * @notice Private function that encodes an integer in big endian binary form with no leading zeroes. + * @dev The zero length check is critical as the case is not considered in the rest of the checking. * @param _uintValue The uint value to be encoded. * @return encodedBytes The encoded uint. */ function _toBinary(uint256 _uintValue) private pure returns (bytes memory encodedBytes) { + if (_uintValue == 0) return new bytes(0); + assembly { let ptr := mload(0x40) - - let i := 0 + let sectionStart := 0 + let sectionLength := 128 for { - } lt(i, 32) { - i := add(i, 1) + } gt(sectionLength, 7) { + sectionLength := shr(1, sectionLength) } { - if iszero(and(shr(sub(248, mul(i, 8)), _uintValue), 0xff)) { + if iszero(shr(sub(256, add(sectionLength, sectionStart)), _uintValue)) { + sectionStart := add(sectionStart, sectionLength) continue } - break } - let length := sub(32, i) + let length := div(sub(256, sectionStart), 8) encodedBytes := ptr mstore(encodedBytes, length) + let actualDataStart := add(encodedBytes, 0x20) + for { let j := 0 } lt(j, length) { j := add(j, 1) } { - let shift := mul(sub(length, add(j, 1)), 8) - let byteToAdd := and(shr(shift, _uintValue), 0xff) - mstore8(add(add(encodedBytes, 0x20), j), byteToAdd) + mstore8(add(actualDataStart, j), and(shr(mul(sub(length, add(j, 1)), 8), _uintValue), 0xff)) } mstore(0x40, add(add(ptr, 0x20), length)) @@ -214,9 +215,7 @@ library RlpEncoder { } lt(i, lengthOfLength) { i := add(i, 1) } { - let shift := mul(8, sub(lengthOfLength, add(i, 1))) - let b := and(shr(shift, _itemLength), 0xff) - mstore8(add(add(encodedBytes, 0x21), i), b) + mstore8(add(add(encodedBytes, 0x21), i), and(shr(mul(8, sub(lengthOfLength, add(i, 1))), _itemLength), 0xff)) } let totalLen := add(lengthOfLength, 1) diff --git a/contracts/test/hardhat/libraries/RlpEncoder.ts b/contracts/test/hardhat/libraries/RlpEncoder.ts index 1383cc192..36982c10c 100644 --- a/contracts/test/hardhat/libraries/RlpEncoder.ts +++ b/contracts/test/hardhat/libraries/RlpEncoder.ts @@ -58,5 +58,32 @@ describe("RlpEncoder Library", () => { const encoded = await contract.encodeInt(123456789n); expect(encoded).equal("0x84075bcd15"); }); + + // TODO random fuzz type tests + it("Encodes a very large positive int correctly", async () => { + const encoded = + await contract.encodeInt(1234567891234567567891234567789123456789123456789123456789123456789123456789n); + expect(encoded).equal("0xa002babd9c27f528a06ee127601e68ddbe8c982496f253de820f6f70b684045f15"); + }); + + it("Encodes a large positive int correctly", async () => { + const encoded = await contract.encodeInt(123456789123456756789123456723456789123456789n); + expect(encoded).equal("0x93058936e53d139a968065bc45c0d9d0540c5f15"); + }); + + it("Encodes 0 correctly", async () => { + const encoded = await contract.encodeInt(0n); + expect(encoded).equal("0x80"); + }); + + it("Encodes 1 correctly", async () => { + const encoded = await contract.encodeInt(1n); + expect(encoded).equal("0x01"); + }); + + it("Encodes 255 correctly", async () => { + const encoded = await contract.encodeInt(255n); + expect(encoded).equal("0x81ff"); + }); }); }); From b5aedaddd93f51e0c4fc6ddaa744515c158b4432 Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 4 Apr 2025 12:39:23 +0100 Subject: [PATCH 15/16] use better naming and remove duplicate pointer --- contracts/src/libraries/RlpEncoder.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index 7b8699e46..8a8aa2987 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -147,7 +147,7 @@ library RlpEncoder { if (_uintValue == 0) return new bytes(0); assembly { - let ptr := mload(0x40) + encodedBytes := mload(0x40) let sectionStart := 0 let sectionLength := 128 for { @@ -162,20 +162,19 @@ library RlpEncoder { } let length := div(sub(256, sectionStart), 8) - encodedBytes := ptr mstore(encodedBytes, length) - let actualDataStart := add(encodedBytes, 0x20) + let writePtr := add(encodedBytes, 0x20) for { let j := 0 } lt(j, length) { j := add(j, 1) } { - mstore8(add(actualDataStart, j), and(shr(mul(sub(length, add(j, 1)), 8), _uintValue), 0xff)) + mstore8(add(writePtr, j), and(shr(mul(sub(length, add(j, 1)), 8), _uintValue), 0xff)) } - mstore(0x40, add(add(ptr, 0x20), length)) + mstore(0x40, add(add(encodedBytes, 0x20), length)) } } From 54d5b2fc666091ce3dff7d0840dd8e4075a2512b Mon Sep 17 00:00:00 2001 From: thedarkjester Date: Fri, 4 Apr 2025 14:22:50 +0100 Subject: [PATCH 16/16] standardise _flatten with assembly --- contracts/src/libraries/RlpEncoder.sol | 53 +++++++++++++------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/contracts/src/libraries/RlpEncoder.sol b/contracts/src/libraries/RlpEncoder.sol index 8a8aa2987..1fae812f4 100644 --- a/contracts/src/libraries/RlpEncoder.sol +++ b/contracts/src/libraries/RlpEncoder.sol @@ -226,42 +226,43 @@ library RlpEncoder { /** * @custom:attribution https://github.com/sammayo/solidity-rlp-encoder - * @notice Flattens a list of byte strings into one byte string. + * @notice Private function that flattens a list of byte strings into one byte string. * @dev mcopy is used for the Cancun EVM fork. See original for other forks. * @param _bytesList List of byte strings to flatten. * @return flattenedBytes The flattened byte string. */ function _flatten(bytes[] memory _bytesList) private pure returns (bytes memory flattenedBytes) { - uint256 bytesListLength = _bytesList.length; - if (bytesListLength == 0) { - return new bytes(0); - } - - uint256 flattenedBytesLength; - uint256 reusableCounter; - for (; reusableCounter < bytesListLength; reusableCounter++) { - unchecked { - flattenedBytesLength += _bytesList[reusableCounter].length; - } - } + uint256 _bytesListLength = _bytesList.length; + if (_bytesListLength == 0) return new bytes(0); - flattenedBytes = new bytes(flattenedBytesLength); - - uint256 flattenedPtr; assembly { - flattenedPtr := add(flattenedBytes, 0x20) - } + flattenedBytes := mload(0x40) + let totalLen := 0 + let offset := add(_bytesList, 0x20) - bytes memory item; - uint256 itemLength; + for { + let i := 0 + } lt(i, _bytesListLength) { + i := add(i, 1) + } { + totalLen := add(totalLen, mload(mload(add(offset, mul(i, 0x20))))) + } + + mstore(flattenedBytes, totalLen) + let writePtr := add(flattenedBytes, 0x20) - for (reusableCounter = 0; reusableCounter < bytesListLength; reusableCounter++) { - item = _bytesList[reusableCounter]; - itemLength = item.length; - assembly { - mcopy(flattenedPtr, add(item, 0x20), itemLength) - flattenedPtr := add(flattenedPtr, itemLength) + for { + let i := 0 + } lt(i, _bytesListLength) { + i := add(i, 1) + } { + let ptr := mload(add(offset, mul(i, 0x20))) + let len := mload(ptr) + mcopy(writePtr, add(ptr, 0x20), len) + writePtr := add(writePtr, len) } + + mstore(0x40, add(flattenedBytes, add(0x20, totalLen))) } } }