keccak: defer squeeze permutation until needed - #23
Conversation
| func TestSpongeReadDefersBoundaryPermutation(t *testing.T) { | ||
| var h sponge |
There was a problem hiding this comment.
Missing the useASM hardware guard. This test drives sponge directly, bypassing the useASM check that Hasher.Write/Hasher.Read perform, so padAndSqueeze -> keccakF1600 reaches the platform assembly unconditionally. On amd64 without BMI1/BMI2 that is keccakF1600BMI2; on non-Darwin arm64 without cpu.ARM64.HasSHA3 (Cortex-A72 / Neoverse N1 — Raspberry Pi 4, Graviton2) that is keccakF1600Sha3. Either way go test ./... dies with SIGILL and takes the whole test binary down instead of skipping. TestXorAndPermute (keccak_asm_test.go:14) already guards this exact case.
| func TestSpongeReadDefersBoundaryPermutation(t *testing.T) { | |
| var h sponge | |
| func TestSpongeReadDefersBoundaryPermutation(t *testing.T) { | |
| if !useASM { | |
| t.Skip("hardware acceleration unavailable on this CPU") | |
| } | |
| var h sponge |
| func BenchmarkSpongeRead(b *testing.B) { | ||
| for _, size := range []int{rate - 1, rate, rate + 1} { | ||
| b.Run(strconv.Itoa(size), func(b *testing.B) { |
There was a problem hiding this comment.
Same SIGILL exposure as the test above: the benchmark builds sponge directly and calls the assembly permutation regardless of useASM. go test -bench . ./... on an amd64 CPU without BMI2, or a non-Darwin arm64 without the SHA3 extension, crashes with an illegal instruction.
| func BenchmarkSpongeRead(b *testing.B) { | |
| for _, size := range []int{rate - 1, rate, rate + 1} { | |
| b.Run(strconv.Itoa(size), func(b *testing.B) { | |
| func BenchmarkSpongeRead(b *testing.B) { | |
| if !useASM { | |
| b.Skip("hardware acceleration unavailable on this CPU") | |
| } | |
| for _, size := range []int{rate - 1, rate, rate + 1} { | |
| b.Run(strconv.Itoa(size), func(b *testing.B) { |
| if h.readIdx != rate { | ||
| t.Fatalf("read index after one full block = %d, want %d", h.readIdx, rate) | ||
| } | ||
|
|
||
| var next [1]byte | ||
| h.Read(next[:]) | ||
| if h.readIdx != 1 { | ||
| t.Fatalf("read index after next byte = %d, want 1", h.readIdx) | ||
| } |
There was a problem hiding this comment.
The test pins readIdx but never checks the squeezed bytes, and no existing test covers the case this PR reorders. TestReadMatchesXCrypto does one single Read per hasher; TestReadMultipleCalls chunks by 37, so it never lands a call boundary exactly on 136. So Read(136) followed by Read(k) — the one sequence where the moved permutation could change output — is unverified against a reference. If the reorder were wrong (e.g. the guard used >=/> incorrectly, or the permutation were dropped), the suite would stay green while producing wrong extended output.
Adding a byte-level assertion to this test would close the gap, e.g. build the reference with sha3.NewLegacyKeccak256().(KeccakState).Read(want) over rate+k bytes and compare against Read(rate) + Read(k) from a Hasher. I ran that locally for first-read sizes 135/136/137/272 and the output matches x/crypto, so the production change itself is correct — this is only about locking it in.
| @@ -0,0 +1,39 @@ | |||
| //go:build (amd64 || arm64) && !purego | |||
There was a problem hiding this comment.
New file duplicates the build tag and package of keccak_asm_test.go, which already holds the asm-only tests. Preference in this repo is to append to the existing _test.go for the package rather than add a parallel file — it keeps the useASM skip convention and the imports in one place.
Summary
Readstops exactly on a 136-byte boundaryWhy
The current loop always computes the next Keccak permutation after copying byte 136, even when the caller asks for no more output. Moving that check to the top of the next loop iteration keeps the output unchanged and computes the next block only when it is requested.
This is an independent, focused split of the squeeze optimization in #13.
TDD
The regression test initially failed with:
Benchmark
Apple M2 Max, Go 1.25.7, representative results:
The exact-boundary case avoids one full permutation. Adjacent sizes remain in the same range; the 135-byte variation is benchmark noise across runs.
Verification
go test ./...go test -race ./...go test -tags purego ./...go vet ./...go mod tidy -diffgolangci-lint run