-
Notifications
You must be signed in to change notification settings - Fork 25
Remove SSTORE2 chunking now that we removed the contract size cap in geth #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ pragma solidity 0.8.24; | |
|
|
||
| import "./ERC721EthscriptionsSequentialEnumerableUpgradeable.sol"; | ||
| import {LibString} from "solady/utils/LibString.sol"; | ||
| import "./libraries/SSTORE2ChunkedStorageLib.sol"; | ||
| import "./libraries/SSTORE2Unlimited.sol"; | ||
| import "./libraries/EthscriptionsRendererLib.sol"; | ||
| import "./EthscriptionsProver.sol"; | ||
| import "./libraries/Predeploys.sol"; | ||
|
|
@@ -16,7 +16,6 @@ import "./libraries/Constants.sol"; | |
| /// @dev Uses ethscription number as token ID and name, while transaction hash remains the primary identifier for function calls | ||
| contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable { | ||
| using LibString for *; | ||
| using SSTORE2ChunkedStorageLib for address[]; | ||
| using EthscriptionsRendererLib for Ethscription; | ||
|
|
||
| // ============================================================= | ||
|
|
@@ -76,8 +75,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable { | |
| /// @dev Ethscription ID (L1 tx hash) => Ethscription data | ||
| mapping(bytes32 => Ethscription) internal ethscriptions; | ||
|
|
||
| /// @dev Content SHA => SSTORE2 pointers array | ||
| mapping(bytes32 => address[]) public contentPointersBySha; | ||
| /// @dev Content SHA => SSTORE2 pointer (single address, no array) | ||
| mapping(bytes32 => address) public contentPointerBySha; | ||
|
|
||
| /// @dev Content URI hash => first ethscription tx hash that used it (for protocol uniqueness check) | ||
| /// @dev bytes32(0) means unused, non-zero means the content URI has been used | ||
|
|
@@ -374,9 +373,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable { | |
| /// @notice Get content for an ethscription | ||
| function getEthscriptionContent(bytes32 ethscriptionId) public view returns (bytes memory) { | ||
| Ethscription storage ethscription = _getEthscriptionOrRevert(ethscriptionId); | ||
| address[] storage pointers = contentPointersBySha[ethscription.contentSha]; | ||
| // 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Overflow on zero address in SSTORE2Unlimited.read breaking empty content handlingThe Additional Locations (1) |
||
| } | ||
|
|
||
| /// @notice Get ethscription details and content in a single call | ||
|
|
@@ -498,21 +496,14 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable { | |
| contentSha = sha256(content); | ||
|
|
||
| // Check if content already exists | ||
| address[] storage existingPointers = contentPointersBySha[contentSha]; | ||
|
|
||
| // Check if content was already stored (pointers array will be non-empty for stored content) | ||
| if (existingPointers.length > 0) { | ||
| address existingPointer = contentPointerBySha[contentSha]; | ||
| if (existingPointer != address(0)) { | ||
| // Content already stored, just return the SHA | ||
| return contentSha; | ||
| } | ||
|
|
||
| // Content doesn't exist, store it using SSTORE2 | ||
| address[] memory pointers = SSTORE2ChunkedStorageLib.store(content); | ||
|
|
||
| // Only store non-empty pointer arrays (empty content doesn't need deduplication) | ||
| if (pointers.length > 0) { | ||
| contentPointersBySha[contentSha] = pointers; | ||
| } | ||
| // Content doesn't exist, store it using SSTORE2Unlimited (handles any size) | ||
| contentPointerBySha[contentSha] = SSTORE2Unlimited.write(content); | ||
|
|
||
| return contentSha; | ||
| } | ||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.4; | ||
|
|
||
| /// @notice Read and write to persistent storage at a fraction of the cost. | ||
| /// @notice Modified to support unlimited content size (up to 4GB) using PUSH4 | ||
| /// @author Modified from Solady (https://github.com/vectorized/solady/blob/main/src/utils/SSTORE2.sol) | ||
| library SSTORE2Unlimited { | ||
|
|
||
|
|
||
| /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
| /* CUSTOM ERRORS */ | ||
| /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
|
||
| /// @dev Unable to deploy the storage contract. | ||
| error DeploymentFailed(); | ||
|
|
||
| /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
| /* WRITE LOGIC */ | ||
| /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
|
||
| /// @dev Writes `data` into the bytecode of a storage contract and returns its address. | ||
| /// Uses a simpler approach with abi.encodePacked for clarity | ||
| function write(bytes memory data) internal returns (address pointer) { | ||
| // Prefix the bytecode with a STOP opcode to ensure it cannot be called. | ||
| bytes memory runtimeCode = abi.encodePacked(hex"00", data); | ||
|
|
||
| bytes memory creationCode = abi.encodePacked( | ||
| //---------------------------------------------------------------------------------------------------------------// | ||
| // Opcode | Opcode + Arguments | Description | Stack View // | ||
| //---------------------------------------------------------------------------------------------------------------// | ||
| // 0x60 | 0x600B | PUSH1 11 | codeOffset // | ||
| // 0x59 | 0x59 | MSIZE | 0 codeOffset // | ||
| // 0x81 | 0x81 | DUP2 | codeOffset 0 codeOffset // | ||
| // 0x38 | 0x38 | CODESIZE | codeSize codeOffset 0 codeOffset // | ||
| // 0x03 | 0x03 | SUB | (codeSize - codeOffset) 0 codeOffset // | ||
| // 0x80 | 0x80 | DUP | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset // | ||
| // 0x92 | 0x92 | SWAP3 | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // | ||
| // 0x59 | 0x59 | MSIZE | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // | ||
| // 0x39 | 0x39 | CODECOPY | 0 (codeSize - codeOffset) // | ||
| // 0xf3 | 0xf3 | RETURN | // | ||
| //---------------------------------------------------------------------------------------------------------------// | ||
| hex"60_0B_59_81_38_03_80_92_59_39_F3", // Returns all code in the contract except for the first 11 (0B in hex) bytes. | ||
| runtimeCode // The bytecode we want the contract to have after deployment. | ||
| ); | ||
|
|
||
| /// @solidity memory-safe-assembly | ||
| assembly { | ||
| // Deploy a new contract with the generated creation code. | ||
| // We start 32 bytes into the code to avoid copying the byte length. | ||
| pointer := create(0, add(creationCode, 32), mload(creationCode)) | ||
|
|
||
| if iszero(pointer) { | ||
| mstore(0x00, 0x30116425) // `DeploymentFailed()`. | ||
| revert(0x1c, 0x04) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
| /* READ LOGIC */ | ||
| /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
|
||
| /// @dev The offset of the data in the bytecode (skipping the STOP opcode). | ||
| uint256 private constant DATA_OFFSET = 1; | ||
|
|
||
| /// @dev Reads all data from the storage contract, skipping the initial STOP opcode. | ||
| function read(address pointer) internal view returns (bytes memory) { | ||
| return readBytecode(pointer, DATA_OFFSET, pointer.code.length - DATA_OFFSET); | ||
| } | ||
|
|
||
| /// @dev Reads bytecode from a contract at a specific offset and size. | ||
| function readBytecode( | ||
| address pointer, | ||
| uint256 start, | ||
| uint256 size | ||
| ) private view returns (bytes memory data) { | ||
| /// @solidity memory-safe-assembly | ||
| assembly { | ||
| // Get a pointer to some free memory. | ||
| data := mload(0x40) | ||
|
|
||
| // Update the free memory pointer to prevent overriding our data. | ||
| // We use and(x, not(31)) as a cheaper equivalent to sub(x, mod(x, 32)). | ||
| // Adding 31 to size and running the result through the logic above ensures | ||
| // the memory pointer remains word-aligned, following the Solidity convention. | ||
| mstore(0x40, add(data, and(add(add(size, 32), 31), not(31)))) | ||
|
|
||
| // Store the size of the data in the first 32 byte chunk of free memory. | ||
| mstore(data, size) | ||
|
|
||
| // Copy the code into memory right after the 32 bytes we used to store the size. | ||
| extcodecopy(pointer, add(data, 32), start, size) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential issue when reading content with a zero address pointer. If
contentPointerBySha[ethscription.contentSha]isaddress(0)(which can happen for empty content or if content was never stored), callingSSTORE2Unlimited.read(pointer)will attempt to read from address(0). In thereadfunction at line 69, this will computepointer.code.length - DATA_OFFSETwhich for address(0) becomes0 - 1, causing an underflow. Consider adding a check:if (pointer == address(0)) return new bytes(0);before the read call.