diff --git a/prover/utils/utils.go b/prover/utils/utils.go index c4d37238021..b75912440ca 100644 --- a/prover/utils/utils.go +++ b/prover/utils/utils.go @@ -271,17 +271,23 @@ func FillRange[T constraints.Integer](dst []T, start T) { } } +// WriterstoEqual returns nil if the two io.WriterTo produce identical +// encodings; otherwise an error describing the difference, or the first +// WriteTo failure. +// +// Each operand must get its own buffer: bytes.Buffer.Bytes() aliases the +// buffer's array across Reset(), so serializing both into one reused buffer +// compares a slice against itself and reports equality for any two encodings +// of the same length. func WriterstoEqual(expected, actual io.WriterTo) error { - var bb bytes.Buffer - if _, err := expected.WriteTo(&bb); err != nil { + var expectedBuf, actualBuf bytes.Buffer + if _, err := expected.WriteTo(&expectedBuf); err != nil { return err } - ab := bb.Bytes() - bb.Reset() - if _, err := actual.WriteTo(&bb); err != nil { + if _, err := actual.WriteTo(&actualBuf); err != nil { return err } - return BytesEqual(ab, bb.Bytes()) + return BytesEqual(expectedBuf.Bytes(), actualBuf.Bytes()) } // BytesEqual between byte slices a,b diff --git a/prover/utils/utils_test.go b/prover/utils/utils_test.go index 590be615e13..c66a83cf49c 100644 --- a/prover/utils/utils_test.go +++ b/prover/utils/utils_test.go @@ -1,7 +1,9 @@ package utils_test import ( + "errors" "fmt" + "io" "testing" "github.com/consensys/linea-monorepo/prover/utils" @@ -75,3 +77,60 @@ func TestNextPowerOfTwoExample(t *testing.T) { }) } } + +// constWriterTo writes a fixed payload, so a test can control the exact bytes +// each operand of WriterstoEqual produces. +type constWriterTo []byte + +func (c constWriterTo) WriteTo(w io.Writer) (int64, error) { + n, err := w.Write(c) + return int64(n), err +} + +func TestWriterstoEqual(t *testing.T) { + for _, tc := range []struct { + name string + expected []byte + actual []byte + equal bool + }{ + {"identical", []byte("abcd"), []byte("abcd"), true}, + // the regression: same length, different content. Serializing both + // operands into one reused buffer compared a slice with itself and + // reported these equal. + {"same length, different content", []byte("abcd"), []byte("wxyz"), false}, + {"differs in last byte only", []byte("abcd"), []byte("abcz"), false}, + {"shorter actual", []byte("abcd"), []byte("ab"), false}, + {"longer actual", []byte("ab"), []byte("abcd"), false}, + {"both empty", []byte{}, []byte{}, true}, + {"empty vs non-empty", []byte{}, []byte("a"), false}, + } { + t.Run(tc.name, func(t *testing.T) { + err := utils.WriterstoEqual(constWriterTo(tc.expected), constWriterTo(tc.actual)) + if tc.equal { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} + +// TestWriterstoEqual_Regression_SameLengthDifferentContent pins the aliasing +// bug where both operands were serialized into one reused bytes.Buffer, so +// any two encodings of the same length compared equal. +func TestWriterstoEqual_Regression_SameLengthDifferentContent(t *testing.T) { + require.Error(t, utils.WriterstoEqual(constWriterTo("abcd"), constWriterTo("wxyz"))) +} + +func TestWriterstoEqualPropagatesWriteErrors(t *testing.T) { + boom := errors.New("boom") + require.ErrorIs(t, utils.WriterstoEqual(failingWriterTo{boom}, constWriterTo("a")), boom) + require.ErrorIs(t, utils.WriterstoEqual(constWriterTo("a"), failingWriterTo{boom}), boom) +} + +// failingWriterTo always fails, to check the error is returned rather than +// swallowed into a false "not equal". +type failingWriterTo struct{ err error } + +func (f failingWriterTo) WriteTo(io.Writer) (int64, error) { return 0, f.err }