diff --git a/espresso/batch_buffer.go b/espresso/batch_buffer.go index a8a3f79e72c60..0232c18d78932 100644 --- a/espresso/batch_buffer.go +++ b/espresso/batch_buffer.go @@ -1,7 +1,9 @@ package espresso import ( + "bytes" "cmp" + "encoding/binary" "slices" "github.com/ethereum-optimism/optimism/op-service/eth" @@ -88,3 +90,40 @@ func (b *BatchBuffer[B]) Pop() *B { return &batch } + +type DummyBatch struct { + number uint64 + l1Origin eth.BlockID +} + +func (b DummyBatch) Number() uint64 { + return b.number +} + +func (b DummyBatch) L1Origin() eth.BlockID { + return b.l1Origin +} + +func (b DummyBatch) Hash() common.Hash { + return common.Hash{} +} + +func (b DummyBatch) Header() *types.Header { + return nil +} + +var b = NewBatchBuffer[DummyBatch]() + +func Fuzz(data []byte) int { + var num uint64 + err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, &num) + + if err != nil { + b.Insert(DummyBatch{number: num, l1Origin: eth.BlockID{Number: num}}, 0) + } else { + b.Peek() + b.Pop() + } + + return 0 +} diff --git a/espresso/batch_buffer_test.go b/espresso/batch_buffer_test.go new file mode 100644 index 0000000000000..0cc40e3d76cb8 --- /dev/null +++ b/espresso/batch_buffer_test.go @@ -0,0 +1,163 @@ +package espresso_test + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/ethereum-optimism/optimism/espresso" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// DummyBatch is a test implementation of the Batch interface +type DummyBatch struct { + number uint64 + l1Origin eth.BlockID +} + +func (b DummyBatch) Number() uint64 { + return b.number +} + +func (b DummyBatch) L1Origin() eth.BlockID { + return b.l1Origin +} + +func (b DummyBatch) Hash() common.Hash { + return common.Hash{} +} + +func (b DummyBatch) Header() *types.Header { + return nil +} + +// Basic batch buffer test +func FuzzBatchBufferBasic(f *testing.F) { + // Generate corpus for batch buffer + f.Add([]byte{0}) + f.Add([]byte{1}) + f.Add([]byte{0, 0, 0, 0, 0, 0, 0, 0}) + f.Add([]byte{1, 0, 0, 0, 0, 0, 0, 0}) + + f.Fuzz(func(t *testing.T, data []byte) { + b := espresso.NewBatchBuffer[DummyBatch]() + var num uint64 + err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, &num) + + if err != nil { + b.Insert(DummyBatch{number: num, l1Origin: eth.BlockID{Number: num}}, 0) + } else { + b.Peek() + b.Pop() + } + }) +} + +// FuzzBatchBuffer tests the BatchBuffer implementation using Go's built-in fuzzer +func FuzzBatchBufferSimple(f *testing.F) { + // Add some seed corpus + f.Add(uint64(0)) + f.Add(uint64(1)) + f.Add(uint64(100)) + f.Add(uint64(1000)) + + // Fuzz test + f.Fuzz(func(t *testing.T, num uint64) { + b := espresso.NewBatchBuffer[DummyBatch]() + + // Test insertion + b.Insert(DummyBatch{number: num, l1Origin: eth.BlockID{Number: num}}, 0) + if b.Len() != 1 { + t.Errorf("Expected buffer length 1, got %d", b.Len()) + } + + // Test peek + batch := b.Peek() + if batch == nil { + t.Fatal("Expected non-nil batch from Peek") + } + if batch.Number() != num { + t.Errorf("Expected batch number %d, got %d", num, batch.Number()) + } + + // Test pop + batch = b.Pop() + if batch == nil { + t.Fatal("Expected non-nil batch from Pop") + } + if batch.Number() != num { + t.Errorf("Expected batch number %d, got %d", num, batch.Number()) + } + if b.Len() != 0 { + t.Errorf("Expected empty buffer after Pop, got length %d", b.Len()) + } + }) +} + +// FuzzBatchBufferInsertMultiple tests inserting multiple batches into the buffer +func FuzzBatchBufferInsertMultiple(f *testing.F) { + // Add some seed corpus + f.Add(uint64(1), uint64(2), uint64(3)) + f.Add(uint64(10), uint64(5), uint64(15)) + + // Fuzz test with multiple insertions + f.Fuzz(func(t *testing.T, num1, num2, num3 uint64) { + b := espresso.NewBatchBuffer[DummyBatch]() + + // Insert batches + b.Insert(DummyBatch{number: num1, l1Origin: eth.BlockID{Number: num1}}, 0) + b.Insert(DummyBatch{number: num2, l1Origin: eth.BlockID{Number: num2}}, 0) + b.Insert(DummyBatch{number: num3, l1Origin: eth.BlockID{Number: num3}}, 0) + + // Test length + if b.Len() != 3 { + t.Errorf("Expected buffer length 3, got %d", b.Len()) + } + + // Test clear + b.Clear() + if b.Len() != 0 { + t.Errorf("Expected empty buffer after Clear, got length %d", b.Len()) + } + }) +} + +// FuzzBatchBufferTryInsert tests the TryInsert method +func FuzzBatchBufferTryInsert(f *testing.F) { + f.Add(uint64(1), uint64(2)) + f.Add(uint64(100), uint64(100)) + + f.Fuzz(func(t *testing.T, num1, num2 uint64) { + b := espresso.NewBatchBuffer[DummyBatch]() + + // Insert first batch + batch1 := DummyBatch{number: num1, l1Origin: eth.BlockID{Number: num1}} + pos, _ := b.TryInsert(batch1) + b.Insert(batch1, pos) + + // Try inserting second batch + batch2 := DummyBatch{number: num2, l1Origin: eth.BlockID{Number: num2}} + pos, exists := b.TryInsert(batch2) + + // If numbers are the same, it should detect as already existing + if num1 == num2 && !exists { + t.Errorf("Expected duplicate batch to be detected") + } + + // If not duplicate, insert it + if !exists { + b.Insert(batch2, pos) + + // Get the batch at position + gotBatch := b.Get(pos) + if gotBatch == nil { + t.Fatal("Expected non-nil batch from Get") + } + if gotBatch.Number() != num2 { + t.Errorf("Expected batch number %d, got %d", num2, gotBatch.Number()) + } + } + }) +} diff --git a/go.mod b/go.mod index 59577b446847d..38fbf80d320d8 100644 --- a/go.mod +++ b/go.mod @@ -108,6 +108,7 @@ require ( github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvyukov/go-fuzz v0.0.0-20240924070022-e577bee5275c // indirect github.com/elastic/gosigar v0.14.3 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect diff --git a/go.sum b/go.sum index dec8ef408683d..3879227e8054a 100644 --- a/go.sum +++ b/go.sum @@ -203,6 +203,8 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvyukov/go-fuzz v0.0.0-20240924070022-e577bee5275c h1:oLpHpHwNuAPvw3bBviEZNrJbigNNi5dRadfZnagGgZI= +github.com/dvyukov/go-fuzz v0.0.0-20240924070022-e577bee5275c/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= diff --git a/justfile b/justfile index 3a7719b2dc45d..c59791f8e57d7 100644 --- a/justfile +++ b/justfile @@ -75,3 +75,9 @@ shellcheck: # Generates a table of contents for the README.md file. toc: md_toc -p github README.md + +fuzz-batch-buffer: + go test -fuzz=FuzzBatchBufferSimple -fuzztime=1m -v ./espresso + go test -fuzz=FuzzBatchBufferInsertMultiple -fuzztime=1m -v ./espresso + go test -fuzz=FuzzBatchBufferTryInsert -fuzztime=1m -v ./espresso + go test -fuzz=FuzzBatchBufferBasic -fuzztime=1m -v ./espresso \ No newline at end of file