From 8f923f8de4e245381409382c18db66213911eff9 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Tue, 15 Sep 2026 17:36:24 +0000 Subject: [PATCH 1/3] Fix 12 spelling and word-slip errors in prose README.md, the LibHashNoAlloc NatSpec that soldeer consumers read, and the two HashPattern docstrings that quote the README heading verbatim. The heading and the docstrings now agree with `testHashContiguousWords`, so a grep for either spelling finds all of the references instead of half. Comments and markdown only: deployed bytecode is byte-identical. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- README.md | 16 ++++++++-------- src/LibHashNoAlloc.sol | 4 ++-- test/HashPattern.t.sol | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f87334c..cd1161a 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ EIP. The key takeaways are: -- We need something determinstic and injective, which can probably be summarised +- We need something deterministic and injective, which can probably be summarised in a single word as "unambiguous" - Hashing bytes is secure by default and any encoding scheme's security can only be less than or equal to the security of the hash of the raw data before it is @@ -199,7 +199,7 @@ identical outcomes. It really just seems to come down to the fact that memory expansion and bulk copying nested/dynamic is not a cheap thing to do. It's typically not millions of -gas, but it can easily be 1-10k+ gas for what is often unneccessary work. +gas, but it can easily be 1-10k+ gas for what is often unnecessary work. Note however that `keccak256` itself is non destructive, it can happily produce a hash on the stack without modifying or allocating any memory at all. Even in @@ -208,7 +208,7 @@ the case that some data is NOT in memory yet and we want to hash it "scratch space for hashing methods". We can put any two words in the scratch space and hash them together without interacting with the allocator at all. -What perhaps is the "fault" of Solidity is that they don't implement `keecak256` +What perhaps is the "fault" of Solidity is that they don't implement `keccak256` for any type other than `bytes` so we are forced to go all the way to Yul and write assembly the moment we want to do anything other than `abi.encode`. @@ -281,11 +281,11 @@ their fields. For example, a `Foo` is ALWAYS 4 words, i.e. 0x80 bytes long. Given the above, we can - Define a pattern for hashing each of the 3 possible memory layouts -- Explain how to handle pointers across non-contigous regions of memory +- Explain how to handle pointers across non-contiguous regions of memory - Discuss the security of the composition - Provide a guide for implementation, maintenance and quality assurance -#### Hashing contigious words +#### Hashing contiguous words In all cases where the size of the data is a known number of words at compile time we are free to simply hash the known memory region. @@ -307,7 +307,7 @@ for known memory regions very naturally. Other than implementation bugs, there's no potential for - Collisions -- Including data what we did not intend to in the hash input +- Including data that we did not intend to include in the hash input - Failing to include some part of the struct Because the size of the data never changes, we can just hardcode it per-type. @@ -390,7 +390,7 @@ dynamic type is an item or field in another struct or dynamic type. Solidity does not allow mixed type lists so all pointers are at least found in predictable positions. We always know at compile time whether something is a pointer or not, either because it's a field at a known offset, or we are dealing -with an individual or list or pointers directly. +with an individual pointer or a list of pointers directly. To reliably handle pointers without allocations: @@ -400,7 +400,7 @@ To reliably handle pointers without allocations: Using our `Foo` struct from above as an example this would look like: -- Hash the first two words as a contigious memory region of known size as `A` +- Hash the first two words as a contiguous memory region of known size as `A` - Hash the dynamic word list `foo_.c` as `B` - Write `A` and `B` to scratch space at `0` and `0x20` respectively - Hash the scratch space to produce `C` diff --git a/src/LibHashNoAlloc.sol b/src/LibHashNoAlloc.sol index a4fbb36..36bbcdf 100644 --- a/src/LibHashNoAlloc.sol +++ b/src/LibHashNoAlloc.sol @@ -17,7 +17,7 @@ bytes32 constant HASH_NIL = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7b /// to use abi.encode, which includes the lengths disambiguating dynamic data. /// Something like `3"abc" + 3"def"` with the length prefixes won't collide with /// `2"ab" + 4"cdef"` but note that ABI provides neither a strong guarantee to -/// be collision resitant on inputs (as far as I know, it's a coincidence that +/// be collision resistant on inputs (as far as I know, it's a coincidence that /// this works), nor an efficient solution. /// /// - Abi encoding is a complex algorithm that is easily 1k+ gas for simple @@ -33,7 +33,7 @@ bytes32 constant HASH_NIL = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7b /// Consider that `hash(hash("abc") + hash("def"))` won't collide with /// `hash(hash("ab") + hash("cdef"))`. It should be easier to convince ourselves /// this is true for all possible pairs of byte strings than it is to convince -/// ourselves that the ABI serialization is never ambigious. Inductively we can +/// ourselves that the ABI serialization is never ambiguous. Inductively we can /// scale this to any data structure that is an ordered composition of byte /// strings, as long as the shape of the composition is fixed and every hash is /// only ever compared with hashes of values of the same type. Across types diff --git a/test/HashPattern.t.sol b/test/HashPattern.t.sol index 0eae606..c8d8243 100644 --- a/test/HashPattern.t.sol +++ b/test/HashPattern.t.sol @@ -20,7 +20,7 @@ struct Foo { /// variable instead of a Yul `let` so that it can be asserted. The oracles use /// neither the library nor the assembly under test. contract HashPatternTest is Test { - /// "Hashing contigious words": a `Foo` is the 4 words `a`, `b` and the + /// "Hashing contiguous words": a `Foo` is the 4 words `a`, `b` and the /// pointers to `c` and `d`, so the hash is the hash of exactly those 4 /// words. The pointer values come from the compiler, not from offsets into /// the struct. @@ -40,7 +40,7 @@ contract HashPatternTest is Test { assertEq(hash_, keccak256(abi.encode(a, b, cPointer, dPointer))); } - /// "Hashing contigious words" for a static array: a `bytes32[3]` is its 3 + /// "Hashing contiguous words" for a static array: a `bytes32[3]` is its 3 /// words with no length prefix, so the word at the pointer is element 0 /// and hashing the 3 words hashes the elements packed. function testHashStaticBytes32Array(bytes32[3] memory arr_) public pure { From b118313e73f075eafc5cf92d17f0858b7c16dff8 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Tue, 15 Sep 2026 22:55:02 +0000 Subject: [PATCH 2/3] Cut the two test docstrings this branch touched Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- test/HashPattern.t.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/HashPattern.t.sol b/test/HashPattern.t.sol index c8d8243..1385b2e 100644 --- a/test/HashPattern.t.sol +++ b/test/HashPattern.t.sol @@ -20,10 +20,6 @@ struct Foo { /// variable instead of a Yul `let` so that it can be asserted. The oracles use /// neither the library nor the assembly under test. contract HashPatternTest is Test { - /// "Hashing contiguous words": a `Foo` is the 4 words `a`, `b` and the - /// pointers to `c` and `d`, so the hash is the hash of exactly those 4 - /// words. The pointer values come from the compiler, not from offsets into - /// the struct. function testHashContiguousWords(uint256 a, address b, uint256[] memory c, bytes memory d) public pure { Foo memory foo_ = Foo(a, b, c, d); bytes32 hash_; @@ -40,9 +36,6 @@ contract HashPatternTest is Test { assertEq(hash_, keccak256(abi.encode(a, b, cPointer, dPointer))); } - /// "Hashing contiguous words" for a static array: a `bytes32[3]` is its 3 - /// words with no length prefix, so the word at the pointer is element 0 - /// and hashing the 3 words hashes the elements packed. function testHashStaticBytes32Array(bytes32[3] memory arr_) public pure { bytes32 hash_; bytes32 first_; From 293d868bade0887b0339f3000ba673e2e8faa5b6 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Wed, 16 Sep 2026 00:02:11 +0000 Subject: [PATCH 3/3] docs: put back the two docstrings the comment sweep deleted whole b118313 deleted both docstrings this branch touched. Neither was this branch's to delete: both predate it on main, and the branch only corrected "contigious" to "contiguous" in them. Deleting them took the memory-layout claims with the typo and left two of the twelve corrections with nothing to correct. They come back without the README heading they quoted, because a `///` block does not reference the README. The claims stand on their own: the four words a `Foo` hashes and where the pointer values come from, and that a `bytes32[3]` carries no length prefix. The spelling fix that made those two of the twelve is therefore moot; the README and NatSpec ones are untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN --- test/HashPattern.t.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/HashPattern.t.sol b/test/HashPattern.t.sol index 1385b2e..4bd164b 100644 --- a/test/HashPattern.t.sol +++ b/test/HashPattern.t.sol @@ -20,6 +20,9 @@ struct Foo { /// variable instead of a Yul `let` so that it can be asserted. The oracles use /// neither the library nor the assembly under test. contract HashPatternTest is Test { + /// A `Foo` is the 4 words `a`, `b` and the pointers to `c` and `d`, so the + /// hash is the hash of exactly those 4 words. The pointer values come from + /// the compiler, not from offsets into the struct. function testHashContiguousWords(uint256 a, address b, uint256[] memory c, bytes memory d) public pure { Foo memory foo_ = Foo(a, b, c, d); bytes32 hash_; @@ -36,6 +39,8 @@ contract HashPatternTest is Test { assertEq(hash_, keccak256(abi.encode(a, b, cPointer, dPointer))); } + /// A `bytes32[3]` is its 3 words with no length prefix, so the word at the + /// pointer is element 0 and hashing the 3 words hashes the elements packed. function testHashStaticBytes32Array(bytes32[3] memory arr_) public pure { bytes32 hash_; bytes32 first_;