Add BytePackLib for efficient content storage#132
Conversation
- Introduced BytePackLib to pack small byte arrays (0-31 bytes) into a bytes32 slot. - Updated Ethscriptions contract to use packed content or SSTORE2 pointers based on content size. - Modified content storage mapping to accommodate both packed data and SSTORE2 pointers. - Enhanced getEthscriptionContent function to handle unpacking of packed data.
There was a problem hiding this comment.
Pull Request Overview
This PR optimizes storage for small ethscription content by packing byte arrays up to 31 bytes directly into a single bytes32 storage slot, avoiding the gas cost of SSTORE2 deployments for small content. The implementation introduces a new BytePackLib library that uses a tag byte to distinguish packed data from SSTORE2 pointers.
- Adds
BytePackLiblibrary for packing/unpacking small byte arrays (0-31 bytes) intobytes32 - Refactors content storage to use packed format for content ≤31 bytes, SSTORE2 for ≥32 bytes
- Updates
contentPointerByShamapping fromaddresstobytes32to support dual-mode storage
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| contracts/src/libraries/BytePackLib.sol | New library implementing byte packing/unpacking with tag-based format |
| contracts/src/Ethscriptions.sol | Updated content storage to use packed format for small content or SSTORE2 pointers for larger content |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Packed data has a non-zero tag byte (1-32) in the first byte | ||
| return uint8(uint256(value >> 248)) != 0; |
There was a problem hiding this comment.
The isPacked function only checks if the first byte is non-zero, which could lead to false positives. While Ethereum addresses (160 bits = 20 bytes) cast to bytes32 have their leftmost 12 bytes as zero under normal circumstances, the tag byte range (1-32) could theoretically overlap with the upper bits if an address somehow had data in those positions. Consider adding an additional check to validate that the tag byte is within the valid range (1-32) to make the distinction more robust: uint8 tag = uint8(uint256(value >> 248)); return tag > 0 && tag <= 32;
| // Packed data has a non-zero tag byte (1-32) in the first byte | |
| return uint8(uint256(value >> 248)) != 0; | |
| // Packed data has a tag byte in the range 1-32 in the first byte | |
| uint8 tag = uint8(uint256(value >> 248)); | |
| return tag > 0 && tag <= 32; |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| contentPointerBySha[contentSha] = SSTORE2Unlimited.write(content); | ||
| // Store based on size | ||
| if (content.length <= 31) { | ||
| // Pack small content directly into bytes32 (0-31 bytes) |
There was a problem hiding this comment.
The condition content.length <= 31 is inconsistent with the BytePackLib implementation which supports packing data up to 31 bytes (if (len >= 32) revert). However, the comment on line 79 states for <32 bytes which is correct. The condition should use < instead of <= to match the boundary of 32 bytes mentioned in the comment, or the comment should be updated to say for <=31 bytes. The current code is correct but the comment inconsistency could cause confusion.
| // Pack small content directly into bytes32 (0-31 bytes) | |
| // Pack small content directly into bytes32 (for <=31 bytes) |
| // For large content (>=32 bytes), it's stored via SSTORE2 with a pointer | ||
| address pointer = eth.getContentPointer(txHash); | ||
| assertTrue(pointer != address(0), "Invalid content pointer"); | ||
| if (content.length >= 32) { |
There was a problem hiding this comment.
The boundary condition uses >= 32 to determine SSTORE2 storage, but line 516 in Ethscriptions.sol uses <= 31 for inline storage. While these are mathematically equivalent, they check opposite conditions. For consistency and clarity, this should use > 31 to match the conceptual model that content > 31 bytes uses SSTORE2, or both places should use the same boundary expression (>= 32 vs <= 31).
| if (content.length >= 32) { | |
| if (content.length > 31) { |
| address pointer = address(uint160(uint256(stored))); | ||
|
|
||
| // Verify it actually has code | ||
| if (pointer.code.length > 0) { |
There was a problem hiding this comment.
[nitpick] The code check pointer.code.length > 0 may return false positives during contract construction. While unlikely in this test context since SSTORE2 contracts are immediately deployed, a more robust check would be pointer.code.length > 0 && pointer != address(0), though the prior address(0) return on line 37 makes this redundant. Consider simplifying by removing this check entirely since isPacked already filters out inline content.
Note
Introduce BytePackLib and switch content storage to a single bytes32 that inlines ≤31B data or stores an SSTORE2 pointer for larger payloads.
contentPointerByShawith unifiedcontentStorageBySha: bytes32storing either packed inline bytes (≤31) or anSSTORE2pointer (≥32).BytePackLibusage ingetEthscriptionContentto detect/unpack inline data; otherwise read viaSSTORE2Unlimited._storeContentto pack small calldata viaBytePackLib.packCalldataor write toSSTORE2Unlimitedand store pointer asbytes32.BytePackLibwithpackCalldata,unpack,isPacked, andpackedLengthfor 0–31 byte payloads.BytePackLib.t.solcovering packing/unpacking, bounds, and format.PackUnpackTest.t.solexamples for pack/unpack behavior.EthscriptionsWithTestFunctionsto expose storage/read helpers compatible with packed-or-pointer format.SSTORE2ContentSizes.t.solto assert inline storage for <32B and pointers for ≥32B, and to validate retrieval/deduplication.Written by Cursor Bugbot for commit 08d416c. This will update automatically on new commits. Configure here.