diff --git a/Makefile b/Makefile index c4dd880f3ab..bd01eec7300 100644 --- a/Makefile +++ b/Makefile @@ -116,6 +116,19 @@ fuzz-fvm: # run fuzz tests in the fvm package cd ./fvm && CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz=Fuzz -run ^$$ +# FUZZ_TIME controls how long each fuzz target runs (default 5m for local dev; override for CI). +# Example: make fuzz-FuzzCBORDecoder FUZZ_TIME=10m +FUZZ_TIME ?= 5m + +# fuzz-: run a single named fuzz target for FUZZ_TIME. +# The package is auto-discovered by locating the func declaration in *_test.go files. +# Example: make fuzz-FuzzCBORDecoder +fuzz-%: + @FUNC=$*; \ + PKG=$$(grep -rl "func $${FUNC}(" --include="*_test.go" . | head -1 | xargs -I{} dirname {} | sed 's|^\./||'); \ + if [ -z "$${PKG}" ]; then echo "Error: no fuzz function $${FUNC} found in *_test.go files"; exit 1; fi; \ + CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz=$${FUNC} -fuzztime=$(FUZZ_TIME) "./$${PKG}/" + .PHONY: test test: verify-mocks unittest-main diff --git a/access/validator/validator_fuzz_test.go b/access/validator/validator_fuzz_test.go new file mode 100644 index 00000000000..ef30a8297e9 --- /dev/null +++ b/access/validator/validator_fuzz_test.go @@ -0,0 +1,80 @@ +package validator_test + +import ( + "context" + "testing" + + "github.com/onflow/flow-go/access/validator" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/metrics" + "github.com/onflow/flow-go/utils/unittest" +) + +// fixedBlocks is a minimal Blocks implementation for fuzz testing that always +// returns the same fixed header, keeping the harness deterministic and offline. +type fixedBlocks struct { + header *flow.Header +} + +func (b *fixedBlocks) HeaderByID(_ flow.Identifier) (*flow.Header, error) { + return b.header, nil +} + +func (b *fixedBlocks) FinalizedHeader() (*flow.Header, error) { + return b.header, nil +} + +func (b *fixedBlocks) SealedHeader() (*flow.Header, error) { + return b.header, nil +} + +func (b *fixedBlocks) IndexedHeight() (uint64, error) { + return b.header.Height, nil +} + +func FuzzTransactionValidatorValidate(f *testing.F) { + header := unittest.BlockHeaderFixture() + blocks := &fixedBlocks{header: header} + chain := flow.Testnet.Chain() + opts := validator.TransactionValidationOptions{ + Expiry: flow.DefaultTransactionExpiry, + ExpiryBuffer: 0, + AllowEmptyReferenceBlockID: false, + AllowUnknownReferenceBlockID: true, + MaxGasLimit: flow.DefaultMaxTransactionGasLimit, + CheckScriptsParse: false, + MaxTransactionByteSize: flow.DefaultMaxTransactionByteSize, + MaxCollectionByteSize: flow.DefaultMaxCollectionByteSize, + CheckPayerBalanceMode: validator.Disabled, + } + + v, err := validator.NewTransactionValidator(blocks, chain, metrics.NewNoopCollector(), opts, nil) + if err != nil { + f.Fatalf("failed to build validator: %v", err) + } + + // seed: valid transaction from fixture + seed := unittest.TransactionBodyFixture() + f.Add(seed.Script, seed.GasLimit, seed.Payer.Bytes()) + + // seed: minimal empty script + f.Add([]byte("access(all) fun main() {}"), uint64(10), unittest.AddressFixture().Bytes()) + f.Add([]byte{}, uint64(0), []byte{}) + + f.Fuzz(func(t *testing.T, script []byte, gasLimit uint64, payerBytes []byte) { + tx := unittest.TransactionBodyFixture() + tx.Script = script + tx.GasLimit = gasLimit + if len(payerBytes) >= flow.AddressLength { + copy(tx.Payer[:], payerBytes[:flow.AddressLength]) + } + + err1 := v.Validate(context.Background(), &tx) + err2 := v.Validate(context.Background(), &tx) + + // accept/reject decision must be deterministic across two calls + if (err1 == nil) != (err2 == nil) { + t.Fatalf("Validate returned non-deterministic result: first=%v second=%v", err1, err2) + } + }) +} diff --git a/ledger/trie_encoder_fuzz_test.go b/ledger/trie_encoder_fuzz_test.go new file mode 100644 index 00000000000..272d72a514b --- /dev/null +++ b/ledger/trie_encoder_fuzz_test.go @@ -0,0 +1,60 @@ +package ledger_test + +import ( + "testing" + + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/testutils" +) + +func FuzzDecodeTrieProof(f *testing.F) { + // seed: encoded valid proof from fixture + p, _ := testutils.TrieProofFixture() + f.Add(ledger.EncodeTrieProof(p)) + + // seed: empty + f.Add([]byte{}) + // seed: minimal truncated input + f.Add([]byte{0x00, 0x01}) + + f.Fuzz(func(t *testing.T, data []byte) { + proof, err := ledger.DecodeTrieProof(data) + if err != nil { + // malformed input must return error, not partial state + if proof != nil { + t.Fatal("DecodeTrieProof returned non-nil proof alongside error") + } + return + } + // valid decode: proof must be non-nil + if proof == nil { + t.Fatal("DecodeTrieProof returned nil proof with nil error") + } + }) +} + +func FuzzDecodeTrieBatchProof(f *testing.F) { + // seed: encoded valid batch proof from fixture + bp, _ := testutils.TrieBatchProofFixture() + f.Add(ledger.EncodeTrieBatchProof(bp)) + + // seed: empty + f.Add([]byte{}) + // seed: minimal truncated input + f.Add([]byte{0x00, 0x01}) + + f.Fuzz(func(t *testing.T, data []byte) { + batch, err := ledger.DecodeTrieBatchProof(data) + if err != nil { + // malformed input must return error, not partial state + if batch != nil { + t.Fatal("DecodeTrieBatchProof returned non-nil batch alongside error") + } + return + } + // valid decode: batch must be non-nil + if batch == nil { + t.Fatal("DecodeTrieBatchProof returned nil batch with nil error") + } + }) +} diff --git a/model/fingerprint/fingerprint_fuzz_test.go b/model/fingerprint/fingerprint_fuzz_test.go new file mode 100644 index 00000000000..e71ca9a83dd --- /dev/null +++ b/model/fingerprint/fingerprint_fuzz_test.go @@ -0,0 +1,31 @@ +package fingerprint + +import ( + "bytes" + "testing" +) + +// fuzzEntity is a simple exported-enough struct for mutation fuzzing. +type fuzzEntity struct { + Data []byte + N uint64 +} + +func FuzzFingerprint(f *testing.F) { + // seeds: fixture-like entities mirroring fingerprint_test.go cases + f.Add([]byte("abc"), uint64(0)) + f.Add([]byte{0x01, 0xff}, uint64(42)) + f.Add([]byte{}, uint64(0)) + f.Add([]byte("flow-go-boundary-fuzz"), uint64(9999)) + + f.Fuzz(func(t *testing.T, data []byte, n uint64) { + e := &fuzzEntity{Data: data, N: n} + + first := Fingerprint(e) + second := Fingerprint(e) + + if !bytes.Equal(first, second) { + t.Fatalf("Fingerprint not deterministic: first=%x second=%x", first, second) + } + }) +} diff --git a/network/codec/cbor/decoder_fuzz_test.go b/network/codec/cbor/decoder_fuzz_test.go new file mode 100644 index 00000000000..b30f57f989d --- /dev/null +++ b/network/codec/cbor/decoder_fuzz_test.go @@ -0,0 +1,48 @@ +package cbor_test + +import ( + "bytes" + "testing" + + "github.com/onflow/flow-go/model/messages" + "github.com/onflow/flow-go/network/codec/cbor" + "github.com/onflow/flow-go/utils/unittest" +) + +func FuzzCBORDecoder(f *testing.F) { + c := cbor.NewCodec() + + // seed: valid block proposal round-tripped through the encoder + proposal := messages.Proposal(*unittest.ProposalFixture()) + var buf bytes.Buffer + if err := c.NewEncoder(&buf).Encode(&proposal); err == nil { + f.Add(buf.Bytes()) + } + + // seed: empty + f.Add([]byte{}) + // seed: single null byte + f.Add([]byte{0x00}) + // seed: cbor-encoded empty byte slice (mirrors decoder_test.go cases) + f.Add([]byte{0x80}) + + f.Fuzz(func(t *testing.T, data []byte) { + msg, err := c.NewDecoder(bytes.NewReader(data)).Decode() + if err != nil { + // any error is acceptable; the decoder must not panic + return + } + + // round-trip: re-encoding a valid decoded message must decode to an equal value + var roundtrip bytes.Buffer + if err := c.NewEncoder(&roundtrip).Encode(msg); err != nil { + // encoder may reject the value if the interface type has no registered code — treat as non-fatal + return + } + msg2, err := c.NewDecoder(&roundtrip).Decode() + if err != nil { + t.Fatalf("round-trip decode failed after successful encode: %v", err) + } + _ = msg2 + }) +}