Skip to content

Commit 346a704

Browse files
committed
Fixes and tests
1 parent 54da1d1 commit 346a704

5 files changed

Lines changed: 343 additions & 14 deletions

File tree

contracts/src/libraries/BytePackLib.sol

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ library BytePackLib {
3131
/// @return data The unpacked bytes data
3232
function unpack(bytes32 packed) internal pure returns (bytes memory data) {
3333
uint256 tag = uint8(uint256(packed >> 248));
34-
if (tag == 0) revert NotPackedData();
34+
if (tag == 0 || tag > 32) revert NotPackedData();
3535

3636
uint256 len = tag - 1;
3737
data = new bytes(len);
@@ -40,19 +40,20 @@ library BytePackLib {
4040
assembly {
4141
// Store the data (shift left by 8 to remove tag byte)
4242
mstore(add(data, 0x20), shl(8, packed))
43-
// Zero the memory after the data for hygiene
44-
mstore(add(add(data, 0x20), len), 0)
43+
// Note: No need to zero memory after the data since new bytes() already zeroes it
44+
// and we're only writing up to 31 bytes into a 32-byte word
4545
}
4646
}
4747
}
4848

4949
/// @notice Check if a bytes32 value is packed data
50-
/// @dev Returns true if the first byte indicates packed data (non-zero tag)
50+
/// @dev Returns true if the first byte indicates packed data (tag between 1-32)
5151
/// @param value The bytes32 value to check
5252
/// @return True if the value is packed data, false otherwise
5353
function isPacked(bytes32 value) internal pure returns (bool) {
54-
// Packed data has a non-zero tag byte (1-32) in the first byte
55-
return uint8(uint256(value >> 248)) != 0;
54+
// Packed data has a tag byte between 1-32 in the first byte
55+
uint256 tag = uint8(uint256(value >> 248));
56+
return tag > 0 && tag <= 32;
5657
}
5758

5859
/// @notice Get the length of packed data without unpacking
@@ -61,7 +62,7 @@ library BytePackLib {
6162
/// @return The length of the packed data (0-31)
6263
function packedLength(bytes32 packed) internal pure returns (uint256) {
6364
uint256 tag = uint8(uint256(packed >> 248));
64-
if (tag == 0) revert NotPackedData();
65+
if (tag == 0 || tag > 32) revert NotPackedData();
6566
return tag - 1;
6667
}
6768
}

contracts/test/BytePackLib.t.sol

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "forge-std/Test.sol";
5+
import "forge-std/console2.sol";
6+
import "../src/libraries/BytePackLib.sol";
7+
8+
/// @title TestWrapper
9+
/// @notice Wrapper contract to expose internal library functions for testing
10+
contract TestWrapper {
11+
function packCalldata(bytes calldata data) external pure returns (bytes32) {
12+
return BytePackLib.packCalldata(data);
13+
}
14+
15+
function unpack(bytes32 packed) external pure returns (bytes memory) {
16+
return BytePackLib.unpack(packed);
17+
}
18+
19+
function isPacked(bytes32 value) external pure returns (bool) {
20+
return BytePackLib.isPacked(value);
21+
}
22+
23+
function packedLength(bytes32 packed) external pure returns (uint256) {
24+
return BytePackLib.packedLength(packed);
25+
}
26+
}
27+
28+
contract BytePackLibTest is Test {
29+
TestWrapper wrapper;
30+
31+
function setUp() public {
32+
wrapper = new TestWrapper();
33+
}
34+
35+
/// @notice Test packing and unpacking for all valid sizes (0-31 bytes)
36+
function test_AllValidSizes() public {
37+
for (uint256 i = 0; i <= 31; i++) {
38+
// Test with incrementing pattern
39+
bytes memory data = new bytes(i);
40+
for (uint256 j = 0; j < i; j++) {
41+
data[j] = bytes1(uint8(j % 256));
42+
}
43+
this.helperPackUnpack(data);
44+
45+
// Also test with all zeros
46+
bytes memory zeros = new bytes(i);
47+
this.helperPackUnpack(zeros);
48+
49+
// Also test with all 0xFF
50+
bytes memory ones = new bytes(i);
51+
for (uint256 j = 0; j < i; j++) {
52+
ones[j] = bytes1(0xFF);
53+
}
54+
this.helperPackUnpack(ones);
55+
}
56+
}
57+
58+
function helperPackUnpack(bytes calldata data) external view {
59+
require(data.length < 32, "Data must be less than 32 bytes");
60+
61+
bytes32 packed = wrapper.packCalldata(data);
62+
63+
// Verify it's marked as packed
64+
assertTrue(wrapper.isPacked(packed), "Should be marked as packed");
65+
66+
// Verify the length is correct
67+
assertEq(wrapper.packedLength(packed), data.length, "Length should match");
68+
69+
// Verify unpacking gives back the original data
70+
bytes memory unpacked = wrapper.unpack(packed);
71+
assertEq(unpacked, data, "Unpacked data should match original");
72+
73+
// Verify the tag byte is correct (length + 1)
74+
uint8 tag = uint8(uint256(packed >> 248));
75+
assertEq(tag, data.length + 1, "Tag should be length + 1");
76+
}
77+
78+
/// @notice Test that packing 32+ bytes reverts
79+
function test_PackingTooLarge_Reverts() public {
80+
for (uint256 size = 32; size <= 40; size++) {
81+
bytes memory data = new bytes(size);
82+
83+
vm.expectRevert(abi.encodeWithSelector(BytePackLib.ContentTooLarge.selector, size));
84+
this.helperPackLarge(data);
85+
}
86+
}
87+
88+
function helperPackLarge(bytes calldata data) external view {
89+
wrapper.packCalldata(data);
90+
}
91+
92+
/// @notice Test that unpacking non-packed data reverts
93+
function test_UnpackNonPacked_Reverts() public {
94+
// Test with zero bytes32 (tag = 0)
95+
bytes32 zero = bytes32(0);
96+
assertFalse(wrapper.isPacked(zero), "Zero should not be packed");
97+
vm.expectRevert(BytePackLib.NotPackedData.selector);
98+
wrapper.unpack(zero);
99+
100+
// Test with an address-like value (no tag byte)
101+
bytes32 addressLike = bytes32(uint256(uint160(address(0x1234567890123456789012345678901234567890))));
102+
assertFalse(wrapper.isPacked(addressLike), "Address should not be packed");
103+
vm.expectRevert(BytePackLib.NotPackedData.selector);
104+
wrapper.unpack(addressLike);
105+
106+
// Test with tag byte > 32 (e.g., 0x21 = 33)
107+
bytes32 invalidTag = bytes32(uint256(0x21) << 248);
108+
assertFalse(wrapper.isPacked(invalidTag), "Tag > 32 should not be packed");
109+
vm.expectRevert(BytePackLib.NotPackedData.selector);
110+
wrapper.unpack(invalidTag);
111+
112+
// Test with tag byte = 255 (maximum uint8)
113+
bytes32 maxTag = bytes32(uint256(0xFF) << 248);
114+
assertFalse(wrapper.isPacked(maxTag), "Tag = 255 should not be packed");
115+
vm.expectRevert(BytePackLib.NotPackedData.selector);
116+
wrapper.unpack(maxTag);
117+
118+
// Test getting length of non-packed data
119+
vm.expectRevert(BytePackLib.NotPackedData.selector);
120+
wrapper.packedLength(zero);
121+
122+
vm.expectRevert(BytePackLib.NotPackedData.selector);
123+
wrapper.packedLength(addressLike);
124+
125+
vm.expectRevert(BytePackLib.NotPackedData.selector);
126+
wrapper.packedLength(invalidTag);
127+
128+
vm.expectRevert(BytePackLib.NotPackedData.selector);
129+
wrapper.packedLength(maxTag);
130+
}
131+
132+
/// @notice Test isPacked detection
133+
function test_IsPacked_Detection() public {
134+
// Pack some data and verify detection
135+
bytes memory testData = hex"74657374"; // "test"
136+
bytes32 packed = wrapper.packCalldata(testData);
137+
assertTrue(wrapper.isPacked(packed), "Packed data should be detected");
138+
139+
// Regular addresses should not be detected as packed
140+
address addr = address(0x1234567890123456789012345678901234567890);
141+
bytes32 addrBytes = bytes32(uint256(uint160(addr)));
142+
assertFalse(wrapper.isPacked(addrBytes), "Address should not be detected as packed");
143+
144+
// Zero should not be detected as packed
145+
assertFalse(wrapper.isPacked(bytes32(0)), "Zero should not be detected as packed");
146+
147+
// Random data without tag byte (first byte is 0x00) should not be detected as packed
148+
bytes32 randomData = bytes32(uint256(0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff));
149+
assertFalse(wrapper.isPacked(randomData), "Random data should not be detected as packed");
150+
}
151+
152+
/// @notice Test the exact packed format
153+
function test_PackedFormat() public {
154+
// Test empty bytes
155+
bytes memory empty = "";
156+
bytes32 packedEmpty = wrapper.packCalldata(empty);
157+
assertEq(uint256(packedEmpty), uint256(bytes32(bytes1(0x01))), "Empty should pack to 0x01 followed by zeros");
158+
159+
// Test single byte 'A' (0x41)
160+
bytes memory single = hex"41";
161+
bytes32 packedSingle = wrapper.packCalldata(single);
162+
// Should be: tag=0x02, data=0x41, rest zeros
163+
assertEq(uint8(uint256(packedSingle >> 248)), 0x02, "Tag for 1 byte should be 2");
164+
assertEq(uint8(uint256(packedSingle >> 240)), 0x41, "Data should be 0x41");
165+
166+
// Test "ABC" (0x414243)
167+
bytes memory abc = hex"414243";
168+
bytes32 packedAbc = wrapper.packCalldata(abc);
169+
// Should be: tag=0x04, data=0x414243, rest zeros
170+
assertEq(uint8(uint256(packedAbc >> 248)), 0x04, "Tag for 3 bytes should be 4");
171+
assertEq(uint8(uint256(packedAbc >> 240)), 0x41, "First byte should be 0x41");
172+
assertEq(uint8(uint256(packedAbc >> 232)), 0x42, "Second byte should be 0x42");
173+
assertEq(uint8(uint256(packedAbc >> 224)), 0x43, "Third byte should be 0x43");
174+
}
175+
176+
/// @notice Test edge cases
177+
function test_EdgeCases() public {
178+
// Test 31 bytes (maximum packable)
179+
bytes memory max = new bytes(31);
180+
for (uint i = 0; i < 31; i++) {
181+
max[i] = bytes1(uint8(i));
182+
}
183+
184+
bytes32 packed = wrapper.packCalldata(max);
185+
assertTrue(wrapper.isPacked(packed), "31 bytes should be packed");
186+
assertEq(wrapper.packedLength(packed), 31, "Length should be 31");
187+
188+
bytes memory unpacked = wrapper.unpack(packed);
189+
assertEq(unpacked, max, "31 bytes should unpack correctly");
190+
191+
// Verify tag is 32 (31 + 1) - the maximum valid tag
192+
assertEq(uint8(uint256(packed >> 248)), 32, "Tag for 31 bytes should be 32");
193+
194+
// Manually create a bytes32 with tag = 32 (boundary case) and verify it's valid
195+
bytes32 dataBytes;
196+
assembly {
197+
dataBytes := mload(add(max, 0x20))
198+
}
199+
bytes32 boundaryTag = bytes32(uint256(32) << 248) | (dataBytes >> 8);
200+
assertTrue(wrapper.isPacked(boundaryTag), "Tag = 32 should be valid packed data");
201+
assertEq(wrapper.packedLength(boundaryTag), 31, "Tag = 32 means 31 bytes of data");
202+
}
203+
}

contracts/test/EthscriptionsWithTestFunctions.sol

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity 0.8.24;
33

44
import "../src/Ethscriptions.sol";
55
import "../src/libraries/SSTORE2Unlimited.sol";
6+
import "../src/libraries/BytePackLib.sol";
67

78
/// @title EthscriptionsWithTestFunctions
89
/// @notice Test contract that extends Ethscriptions with additional functions for testing
@@ -14,18 +15,40 @@ contract EthscriptionsWithTestFunctions is Ethscriptions {
1415
/// @dev Test-only function to check if content exists
1516
function hasContent(bytes32 ethscriptionId) external view returns (bool) {
1617
Ethscription storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
17-
return contentPointerBySha[ethscription.contentSha] != address(0);
18+
return contentStorageBySha[ethscription.contentSha] != bytes32(0);
1819
}
1920

20-
/// @notice Get the content pointer for an ethscription
21+
/// @notice Get the content storage value for an ethscription
22+
/// @dev Test-only function to inspect storage (either packed bytes or SSTORE2 address)
23+
function getContentStorage(bytes32 ethscriptionId) external view returns (bytes32) {
24+
Ethscription storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
25+
return contentStorageBySha[ethscription.contentSha];
26+
}
27+
28+
/// @notice Get the content pointer for an ethscription (only for SSTORE2 stored content)
2129
/// @dev Test-only function to inspect SSTORE2 address
2230
function getContentPointer(bytes32 ethscriptionId) external view returns (address) {
2331
Ethscription storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
24-
return contentPointerBySha[ethscription.contentSha];
32+
bytes32 stored = contentStorageBySha[ethscription.contentSha];
33+
34+
// Check if it's inline content using BytePackLib
35+
if (BytePackLib.isPacked(stored)) {
36+
// It's packed inline content, not a pointer
37+
return address(0);
38+
}
39+
40+
// It's a pointer to SSTORE2 contract
41+
address pointer = address(uint160(uint256(stored)));
42+
43+
// Verify it actually has code
44+
if (pointer.code.length > 0) {
45+
return pointer;
46+
}
47+
return address(0);
2548
}
2649

2750
/// @notice Read content directly
28-
/// @dev Test-only function to read content from SSTORE2
51+
/// @dev Test-only function to read content
2952
/// @param ethscriptionId The ethscription ID (L1 tx hash)
3053
/// @return The content data
3154
function readContent(bytes32 ethscriptionId) external view returns (bytes memory) {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "forge-std/Test.sol";
5+
import "forge-std/console2.sol";
6+
7+
contract PackUnpackTest is Test {
8+
9+
function packContent(bytes memory data) internal pure returns (bytes32 out) {
10+
assembly {
11+
let len := mload(data)
12+
// Allow 0..31 bytes
13+
if lt(len, 32) {
14+
// out = (len+1)<<248 | (first 31 bytes of data)
15+
// mload returns 32 bytes; shr(8, ...) drops the last byte to get first 31 bytes
16+
out := or(shl(248, add(len, 1)), shr(8, mload(add(data, 0x20))))
17+
}
18+
}
19+
}
20+
21+
function unpackContent(bytes32 packed) internal pure returns (bytes memory out) {
22+
uint256 tag = uint8(uint256(packed >> 248)); // Top byte
23+
if (tag == 0) return out; // Not inline
24+
uint256 len = tag - 1;
25+
out = new bytes(len);
26+
if (len == 0) return out;
27+
assembly {
28+
// Write the 31 data bytes (tag removed) to out[0:]
29+
mstore(add(out, 0x20), shl(8, packed))
30+
// Optional hygiene: zero the word immediately after the data
31+
mstore(add(add(out, 0x20), len), 0)
32+
}
33+
}
34+
35+
function test_PackUnpack_HelloWorld() public {
36+
bytes memory original = bytes("Hello, World!");
37+
console2.log("Original length:", original.length);
38+
console2.logBytes(original);
39+
40+
bytes32 packed = packContent(original);
41+
console2.log("Packed:");
42+
console2.logBytes32(packed);
43+
44+
bytes memory unpacked = unpackContent(packed);
45+
console2.log("Unpacked length:", unpacked.length);
46+
console2.logBytes(unpacked);
47+
48+
assertEq(unpacked, original, "Unpacked data should match original");
49+
}
50+
51+
function test_PackUnpack_Empty() public {
52+
bytes memory original = bytes("");
53+
console2.log("Original length:", original.length);
54+
55+
bytes32 packed = packContent(original);
56+
console2.log("Packed:");
57+
console2.logBytes32(packed);
58+
59+
bytes memory unpacked = unpackContent(packed);
60+
console2.log("Unpacked length:", unpacked.length);
61+
62+
assertEq(unpacked, original, "Unpacked data should match original");
63+
}
64+
65+
function test_PackUnpack_SingleByte() public {
66+
bytes memory original = bytes("A");
67+
console2.log("Original length:", original.length);
68+
console2.logBytes(original);
69+
70+
bytes32 packed = packContent(original);
71+
console2.log("Packed:");
72+
console2.logBytes32(packed);
73+
74+
bytes memory unpacked = unpackContent(packed);
75+
console2.log("Unpacked length:", unpacked.length);
76+
console2.logBytes(unpacked);
77+
78+
assertEq(unpacked, original, "Unpacked data should match original");
79+
}
80+
81+
function test_PackUnpack_31Bytes() public {
82+
bytes memory original = bytes("1234567890123456789012345678901"); // 31 bytes
83+
console2.log("Original length:", original.length);
84+
85+
bytes32 packed = packContent(original);
86+
console2.log("Packed:");
87+
console2.logBytes32(packed);
88+
89+
bytes memory unpacked = unpackContent(packed);
90+
console2.log("Unpacked length:", unpacked.length);
91+
92+
assertEq(unpacked, original, "Unpacked data should match original");
93+
}
94+
}

0 commit comments

Comments
 (0)