Commit 827fc7b0 authored by clabby's avatar clabby

Add `go-fuzz`

:broom:
parent 9ee4f3d7
---
'@eth-optimism/contracts-bedrock': patch
---
Adds a go package to generate fuzz inputs for the Bedrock contract tests.
......@@ -19,6 +19,7 @@ use (
./op-proposer
./op-service
./proxyd
./packages/contracts-bedrock/go-fuzz
)
replace github.com/ethereum/go-ethereum v1.10.26 => github.com/ethereum-optimism/op-geth v0.0.0-20221205191237-0678a130d790
......
......@@ -10,3 +10,4 @@ src/contract-artifacts.ts
tmp-artifacts
deployments/mainnet-forked
deploy-config/mainnet-forked.json
go-fuzz/fuzz
# `go-fuzz`
A lightweight input fuzzing utility used for testing various Bedrock contracts.
<pre>
├── go-fuzz
│ ├── <a href="./cmd">cmd</a>: `go-fuzz`'s binary
│ └── <a href="./trie">trie</a>: Utility for generating random merkle trie roots / inclusion proofs
</pre>
## Usage
To generate an abi-encoded fuzz case, pass in a mode via the `-m` flag as well as an optional variant via the `-v` flag.
### Available Modes
#### `trie`
> **Note**
> Variant required for `trie` mode.
| Variant | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `valid` | Generate a test case with a valid proof of inclusion for the k/v pair in the trie. |
| `extra_proof_elems` | Generate an invalid test case with an extra proof element attached to an otherwise valid proof of inclusion for the passed k/v. |
| `corrupted_proof` | Generate an invalid test case where the proof is malformed. |
| `invalid_data_remainder` | Generate an invalid test case where a random element of the proof has more bytes than the length designates within the RLP list encoding. |
| `invalid_large_internal_hash` | Generate an invalid test case where a long proof element is incorrect for the root. |
| `invalid_internal_node_hash` | Generate an invalid test case where a small proof element is incorrect for the root. |
| `prefixed_valid_key` | Generate a valid test case with a key that has been given a random prefix |
| `empty_key` | Generate a valid test case with a proof of inclusion for an empty key. |
| `partial_proof` | Generate an invalid test case with a partially correct proof |
package main
import (
"flag"
"log"
t "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/go-fuzz/trie"
)
// Mode enum
const (
// Enables the `trie` fuzzer
trie string = "trie"
)
func main() {
mode := flag.String("m", "", "Fuzzer mode")
variant := flag.String("v", "", "Mode variant")
flag.Parse()
if len(*mode) < 1 {
log.Fatal("Must pass a mode for the fuzzer!")
}
switch *mode {
case trie:
t.FuzzTrie(*variant)
default:
log.Fatal("Invalid mode!")
}
}
module github.com/ethereum-optimism/optimism/packages/contracts-bedrock/go-fuzz
go 1.19
require github.com/ethereum/go-ethereum v1.10.26
require (
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/VictoriaMetrics/fastcache v1.6.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/tsdb v0.7.1 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tklauser/go-sysconf v0.3.5 // indirect
github.com/tklauser/numcpus v0.2.2 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
)
This diff is collapsed.
package trie
import (
"crypto/rand"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
// Variant enum
const (
// Generate a test case with a valid proof of inclusion for the k/v pair in the trie.
valid string = "valid"
// Generate an invalid test case with an extra proof element attached to an otherwise
// valid proof of inclusion for the passed k/v.
extraProofElems = "extra_proof_elems"
// Generate an invalid test case where the proof is malformed.
corruptedProof = "corrupted_proof"
// Generate an invalid test case where a random element of the proof has more bytes than the
// length designates within the RLP list encoding.
invalidDataRemainder = "invalid_data_remainder"
// Generate an invalid test case where a long proof element is incorrect for the root.
invalidLargeInternalHash = "invalid_large_internal_hash"
// Generate an invalid test case where a small proof element is incorrect for the root.
invalidInternalNodeHash = "invalid_internal_node_hash"
// Generate a valid test case with a key that has been given a random prefix
prefixedValidKey = "prefixed_valid_key"
// Generate a valid test case with a proof of inclusion for an empty key.
emptyKey = "empty_key"
// Generate an invalid test case with a partially correct proof
partialProof = "partial_proof"
)
// Generate an abi-encoded `trieTestCase` of a specified variant
func FuzzTrie(variant string) {
if len(variant) < 1 {
log.Fatal("Must pass a variant to the trie fuzzer!")
}
var testCase trieTestCase
switch variant {
case valid:
testCase = genTrieTestCase(false)
break
case extraProofElems:
testCase = genTrieTestCase(false)
// Duplicate the last element of the proof
testCase.Proof = append(testCase.Proof, [][]byte{testCase.Proof[len(testCase.Proof)-1]}...)
break
case corruptedProof:
testCase = genTrieTestCase(false)
// Re-encode a random element within the proof
idx := randRange(0, int64(len(testCase.Proof)))
encoded, _ := rlp.EncodeToBytes(testCase.Proof[idx])
testCase.Proof[idx] = encoded
break
case invalidDataRemainder:
testCase = genTrieTestCase(false)
// Alter true length of random proof element by appending random bytes
// Do not update the encoded length
idx := randRange(0, int64(len(testCase.Proof)))
bytes := make([]byte, randRange(1, 512))
rand.Read(bytes)
testCase.Proof[idx] = append(testCase.Proof[idx], bytes...)
break
case invalidLargeInternalHash:
testCase = genTrieTestCase(false)
// Clobber 4 bytes within a list element of a random proof element
// TODO: Improve this by decoding the proof elem and choosing random
// bytes to overwrite.
idx := randRange(1, int64(len(testCase.Proof)))
b := make([]byte, 4)
rand.Read(b)
testCase.Proof[idx] = append(
testCase.Proof[idx][:20],
append(
b,
testCase.Proof[idx][24:]...,
)...,
)
break
case invalidInternalNodeHash:
testCase = genTrieTestCase(false)
// Assign the last proof element to an encoded list containing a
// random 29 byte value
b := make([]byte, 29)
rand.Read(b)
e, _ := rlp.EncodeToBytes(b)
testCase.Proof[len(testCase.Proof)-1] = append([]byte{0xc0 + 30}, e...)
break
case prefixedValidKey:
testCase = genTrieTestCase(false)
bytes := make([]byte, randRange(1, 16))
rand.Read(bytes)
testCase.Key = append(bytes, testCase.Key...)
break
case emptyKey:
testCase = genTrieTestCase(true)
break
case partialProof:
testCase = genTrieTestCase(false)
// Cut the proof in half
proofLen := len(testCase.Proof)
newProof := make([][]byte, proofLen/2)
for i := 0; i < proofLen/2; i++ {
newProof[i] = testCase.Proof[i]
}
testCase.Proof = newProof
break
default:
log.Fatal("Invalid variant passed to trie fuzzer!")
}
// Print encoded test case with no newline so that foundry's FFI can read the output
fmt.Print(testCase.AbiEncode())
}
// Generate a random test case for Bedrock's MerkleTrie verifier.
func genTrieTestCase(selectEmptyKey bool) trieTestCase {
// Create an empty merkle trie
memdb := memorydb.New()
randTrie := trie.NewEmpty(trie.NewDatabase(memdb))
// Get a random number of elements to put into the trie
randN := randRange(2, 1024)
// Get a random key/value pair to generate a proof of inclusion for
randSelect := randRange(0, randN)
// Create a fixed-length key as well as a randomly-sized value
// We create these out of the loop to reduce mem allocations.
randKey := make([]byte, 32)
randValue := make([]byte, randRange(2, 1024))
// Randomly selected key/value for proof generation
var key []byte
var value []byte
// Add `randN` elements to the trie
for i := int64(0); i < randN; i++ {
// Randomize the contents of `randKey` and `randValue`
rand.Read(randKey)
rand.Read(randValue)
// Clear the selected key if `selectEmptyKey` is true
if i == randSelect && selectEmptyKey {
randKey = make([]byte, 0)
}
// Insert the random k/v pair into the trie
if err := randTrie.TryUpdate(randKey, randValue); err != nil {
log.Fatal("Error adding key-value pair to trie")
}
// If this is our randomly selected k/v pair, store it in `key` & `value`
if i == randSelect {
key = randKey
value = randValue
}
}
// Generate proof for `key`'s inclusion in our trie
var proof proofList
if err := randTrie.Prove(key, 0, &proof); err != nil {
log.Fatal("Error creating proof for randomly selected key's inclusion in generated trie")
}
// Create our test case with the data collected
testCase := trieTestCase{
Root: randTrie.Hash(),
Key: key,
Value: value,
Proof: proof,
}
return testCase
}
// Represents a test case for bedrock's `MerkleTrie.sol`
type trieTestCase struct {
Root common.Hash
Key []byte
Value []byte
Proof [][]byte
}
// Tuple type to encode `TrieTestCase`
var (
trieTestCaseTuple, _ = abi.NewType("tuple", "TrieTestCase", []abi.ArgumentMarshaling{
{Name: "root", Type: "bytes32"},
{Name: "key", Type: "bytes"},
{Name: "value", Type: "bytes"},
{Name: "proof", Type: "bytes[]"},
})
encoder = abi.Arguments{
{Type: trieTestCaseTuple},
}
)
// Encodes the trieTestCase as the `trieTestCaseTuple`.
func (t *trieTestCase) AbiEncode() string {
// Encode the contents of the struct as a tuple
packed, err := encoder.Pack(&t)
if err != nil {
log.Fatalf("Error packing TrieTestCase: %v", err)
}
// Remove the pointer and encode the packed bytes as a hex string
return hexutil.Encode(packed[32:])
}
// Helper that generates a cryptographically secure random 64-bit integer
// between the range [min, max)
func randRange(min int64, max int64) int64 {
r, err := rand.Int(rand.Reader, new(big.Int).Sub(new(big.Int).SetInt64(max), new(big.Int).SetInt64(min)))
if err != nil {
log.Fatal("Failed to generate random number within bounds")
}
return (new(big.Int).Add(r, new(big.Int).SetInt64(min))).Int64()
}
// Custom type to write the generated proof to
type proofList [][]byte
func (n *proofList) Put(key []byte, value []byte) error {
*n = append(*n, value)
return nil
}
func (n *proofList) Delete(key []byte) error {
panic("not supported")
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment