solady icon indicating copy to clipboard operation
solady copied to clipboard

✨ SSTORE2 should chunk saves to circumvent the 4M gas limit!

Open RogerPodacter opened this issue 1 year ago • 3 comments

This gas limit is artificial and users shouldn't have to juggle it themselves. This is similar to automatically increasing the buffer when the user appends "too much" in DynamicBuffer. Many users are sheeple and cannot handle the complexity themselves!

Like with DynamicBuffer, SSTORE2 could operate with a custom struct that abstracts away these inessential details.

Something like:

struct StoredBytes {
    address[] locations;
}

using SSTORE2 for SSTORE2.StoredBytes;

function test(bytes memory input) public {
    StoredBytes memory blob = SSTORE2.coolWrite(input);
    
    blob.read();
}

Yes, this would mean that our children will store data on Ethereum without truly learning it goes into an address and how weird and cute that is, but I think it's worth it!

Here's something I found that handles the read side:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/**
 * @title AddressChunks
 * @author @xtremetom
 * @notice Reads chunk pointers and merges their values
 */
library AddressChunks {
    function mergeChunks(address[] memory chunks)
        internal
        view
        returns (bytes memory o_code)
    {
        unchecked {
            assembly {
                let len := mload(chunks)
                let totalSize := 0x20
                let size := 0
                o_code := mload(0x40)

                // loop through all chunk addresses
                // - get address
                // - get data size
                // - get code and add to o_code
                // - update total size
                let targetChunk := 0
                for {
                    let i := 0
                } lt(i, len) {
                    i := add(i, 1)
                } {
                    targetChunk := mload(add(chunks, add(0x20, mul(i, 0x20))))
                    size := sub(extcodesize(targetChunk), 1)
                    extcodecopy(targetChunk, add(o_code, totalSize), 1, size)
                    totalSize := add(totalSize, size)
                }

                // update o_code size
                mstore(o_code, sub(totalSize, 0x20))
                // store o_code
                mstore(0x40, add(o_code, and(add(totalSize, 0x1f), not(0x1f))))
            }
        }
    }
}

Thoughts?

RogerPodacter avatar May 02 '23 19:05 RogerPodacter

Have been thinking about this myself, I'm a fan for sure 👏. While impractical due to gas, this could be used to store a somewhat high resolution image on-chain.

0xClandestine avatar May 02 '23 22:05 0xClandestine

Yes that's a good point, the client should probably do the chunking to save gas. So here is my revised proposal which I think is pretty good! Would be even better integrated into SSTORE2 of course.

library ChunkedSSTORE2Lib {
    struct ByteChunks {
        address[] locations;
    }
    
    function writeChunks(bytes[] memory chunks) internal returns (ByteChunks memory) {
        address[] memory locations = new address[](chunks.length);
        
        for (uint i; i < locations.length;) {
            locations[i] = SSTORE2.write(chunks[i]);
            unchecked {++i;}
        }
        
        return ChunkedSSTORE2Lib.ByteChunks(locations);
    }
    
    // AddressChunks by @xtremetom
    // https://github.com/intartnft/scripty.sol/blob/main/contracts/scripty/utils/AddressChunks.sol
    function readChunks(ByteChunks memory blob) internal view returns (bytes memory combinedBytes) {
        address[] memory chunks = blob.locations;
        
        unchecked {
            assembly {
                let len := mload(chunks)
                let totalSize := 0x20
                let size := 0
                combinedBytes := mload(0x40)

                // loop through all chunk addresses
                // - get address
                // - get data size
                // - get code and add to o_code
                // - update total size
                let targetChunk := 0
                for {
                    let i := 0
                } lt(i, len) {
                    i := add(i, 1)
                } {
                    targetChunk := mload(add(chunks, add(0x20, mul(i, 0x20))))
                    size := sub(extcodesize(targetChunk), 1)
                    extcodecopy(targetChunk, add(combinedBytes, totalSize), 1, size)
                    totalSize := add(totalSize, size)
                }

                // update o_code size
                mstore(combinedBytes, sub(totalSize, 0x20))
                // store o_code
                mstore(0x40, add(combinedBytes, and(add(totalSize, 0x1f), not(0x1f))))
            }
        }
    }
}

RogerPodacter avatar May 03 '23 13:05 RogerPodacter

I am using a similar approach in EthFS, with the additional feature of allowing you to specify exactly the bytecode start/end for each address to support reusing bytecode from any arbitrary contract, not just ones stored via SSTORE2: https://github.com/holic/ethfs/blob/main/packages/contracts/src/File.sol

frolic avatar Jan 10 '24 14:01 frolic