Remove SSTORE2 chunking now that we removed the contract size cap in geth#131
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR refactors the content storage mechanism from a chunked SSTORE2 approach to a single-contract SSTORE2Unlimited implementation that can handle arbitrarily large content (up to 4GB).
Key changes:
- Replaces the
SSTORE2ChunkedStorageLibwith a newSSTORE2Unlimitedlibrary that stores all content in a single contract deployment - Simplifies the storage model from
mapping(bytes32 => address[])tomapping(bytes32 => address)for content pointers - Adds comprehensive test suite for various content sizes from 0 bytes to 2MB
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/src/libraries/SSTORE2Unlimited.sol | New library implementing unlimited-size SSTORE2 storage using a single contract deployment |
| contracts/src/libraries/SSTORE2ChunkedStorageLib.sol | Removed old chunked storage library |
| contracts/src/Ethscriptions.sol | Updated to use single pointer instead of array of pointers for content storage |
| contracts/test/EthscriptionsWithTestFunctions.sol | Updated test helper functions from chunk-based to single-pointer model |
| contracts/test/SSTORE2ContentSizes.t.sol | New comprehensive test suite for various content sizes |
| contracts/test/EthscriptionsJson.t.sol | Updated tests from chunk verification to single-pointer verification; improved loop efficiency |
| contracts/foundry.toml | Increased gas limit to maximum value for testing large content |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| address[] storage pointers = contentPointersBySha[ethscription.contentSha]; | ||
| // Empty content is valid - returns "" for empty pointers array | ||
| return pointers.read(); | ||
| address pointer = contentPointerBySha[ethscription.contentSha]; |
There was a problem hiding this comment.
Potential issue when reading content with a zero address pointer. If contentPointerBySha[ethscription.contentSha] is address(0) (which can happen for empty content or if content was never stored), calling SSTORE2Unlimited.read(pointer) will attempt to read from address(0). In the read function at line 69, this will compute pointer.code.length - DATA_OFFSET which for address(0) becomes 0 - 1, causing an underflow. Consider adding a check: if (pointer == address(0)) return new bytes(0); before the read call.
| address pointer = contentPointerBySha[ethscription.contentSha]; | |
| address pointer = contentPointerBySha[ethscription.contentSha]; | |
| if (pointer == address(0)) { | |
| return new bytes(0); | |
| } |
| assertEq(retrievedContent[0], originalContent[0], "First byte mismatch"); | ||
| assertEq( | ||
| retrievedContent[retrievedContent.length - 1], | ||
| originalContent[originalContent.length - 1], | ||
| "Last byte mismatch" | ||
| ); |
There was a problem hiding this comment.
Potential array out-of-bounds access for empty content. The _verifyContent function is called when size > 0 (line 142), but it doesn't guard against empty arrays. If an empty content somehow reaches this function, accessing index 0 or length - 1 will revert. While the caller checks size > 0, adding a size check at the start of this function would make it more defensive.
| for (uint i = 0; i < size; i++) { | ||
| assertEq(retrievedContent[i], originalContent[i], "Byte mismatch"); |
There was a problem hiding this comment.
Missing unchecked block for loop increment. Since i is bounded by size which is at most 1024 in this branch, and the loop is already incrementing with i++, wrapping the increment in an unchecked block would save gas and be consistent with the pattern used elsewhere in the codebase (e.g., lines 217-219, 437-439).
| for (uint i = 0; i < size; i++) { | |
| assertEq(retrievedContent[i], originalContent[i], "Byte mismatch"); | |
| for (uint i = 0; i < size;) { | |
| assertEq(retrievedContent[i], originalContent[i], "Byte mismatch"); | |
| unchecked { ++i; } |
| for (uint256 i = 0; i < sizes.length; i++) { | ||
| _testDeduplication(sizes[i], string.concat("Size: ", vm.toString(sizes[i]))); |
There was a problem hiding this comment.
Missing unchecked block for loop increment. The loop counter i is bounded by the fixed-size array length (5 elements), so overflow is impossible. Wrapping the increment in an unchecked block would save gas and be consistent with the pattern used elsewhere in the file (e.g., lines 217-219).
| for (uint256 i = 0; i < sizes.length; i++) { | |
| _testDeduplication(sizes[i], string.concat("Size: ", vm.toString(sizes[i]))); | |
| for (uint256 i = 0; i < sizes.length;) { | |
| _testDeduplication(sizes[i], string.concat("Size: ", vm.toString(sizes[i]))); | |
| unchecked { ++i; } |
| // Empty content is valid - returns "" for empty pointers array | ||
| return pointers.read(); | ||
| address pointer = contentPointerBySha[ethscription.contentSha]; | ||
| return SSTORE2Unlimited.read(pointer); |
There was a problem hiding this comment.
Bug: Overflow on zero address in SSTORE2Unlimited.read breaking empty content handling
The SSTORE2Unlimited.read function underflows when called with address(0), as pointer.code.length - DATA_OFFSET becomes 0 - 1. This causes getEthscriptionContent to revert for existing ethscriptions with empty content, because contentPointerBySha returns address(0) for these. This breaks backward compatibility, as the old system gracefully returned empty bytes for such cases.
Note
Switches content storage to a single SSTORE2 pointer via SSTORE2Unlimited, removes chunking, and updates/expands tests to cover various sizes and deduplication.
SSTORE2Unlimited.Ethscriptions.sol: switchcontentPointersBySha: bytes32 => address[]tocontentPointerBySha: bytes32 => address; update read/write to useSSTORE2Unlimited.read/write.libraries/SSTORE2ChunkedStorageLib.sol; addlibraries/SSTORE2Unlimited.sol(single-contract, unlimited-size storage).EthscriptionsWithTestFunctions.sol: remove chunk helpers; addhasContent,getContentPointer,readContent.EthscriptionsJson.t.solto validate single-pointer storage and JSON outputs (no chunk assumptions); optimize loops.SSTORE2ContentSizes.t.sol: comprehensive size tests (empty → 2MB), retrieval, and deduplication gas checks.foundry.toml: setgas_limitto18446744073709551615.Written by Cursor Bugbot for commit 356f2c7. This will update automatically on new commits. Configure here.