Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions prover/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions prover/utils/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package utils_test

import (
"errors"
"fmt"
"io"
"testing"

"github.com/consensys/linea-monorepo/prover/utils"
Expand Down Expand Up @@ -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 }