fuzz_parsers_test.go 9.13 KB
Newer Older
1 2 3 4 5 6 7
package derive

import (
	"bytes"
	"math/big"
	"testing"

8 9 10
	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/require"

11
	"github.com/ethereum/go-ethereum/accounts/abi"
12 13 14 15 16 17 18
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/rawdb"
	"github.com/ethereum/go-ethereum/core/state"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/core/vm/runtime"
	"github.com/ethereum/go-ethereum/crypto"
19 20 21 22

	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
	"github.com/ethereum-optimism/optimism/op-node/eth"
	"github.com/ethereum-optimism/optimism/op-node/testutils"
23 24 25 26 27 28 29
)

var (
	pk, _                  = crypto.GenerateKey()
	addr                   = common.Address{0x42, 0xff}
	opts, _                = bind.NewKeyedTransactorWithChainID(pk, common.Big1)
	from                   = crypto.PubkeyToAddress(pk.PublicKey)
30 31
	portalContract, _      = bindings.NewOptimismPortal(addr, nil)
	l1BlockInfoContract, _ = bindings.NewL1Block(addr, nil)
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
)

func cap_byte_slice(b []byte, c int) []byte {
	if len(b) <= c {
		return b
	} else {
		return b[:c]
	}
}

func BytesToBigInt(b []byte) *big.Int {
	return new(big.Int).SetBytes(cap_byte_slice(b, 32))
}

// FuzzL1InfoRoundTrip checks that our encoder round trips properly
func FuzzL1InfoRoundTrip(f *testing.F) {
	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, hash []byte, seqNumber uint64) {
		in := L1BlockInfo{
			Number:         number,
			Time:           time,
			BaseFee:        BytesToBigInt(baseFee),
			BlockHash:      common.BytesToHash(hash),
			SequenceNumber: seqNumber,
		}
		enc, err := in.MarshalBinary()
		if err != nil {
			t.Fatalf("Failed to marshal binary: %v", err)
		}
		var out L1BlockInfo
		err = out.UnmarshalBinary(enc)
		if err != nil {
			t.Fatalf("Failed to unmarshal binary: %v", err)
		}
65
		if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
66 67 68 69 70 71 72 73 74
			t.Fatalf("The data did not round trip correctly. in: %v. out: %v", in, out)
		}

	})
}

// FuzzL1InfoAgainstContract checks the custom marshalling functions against the contract
// bindings to ensure that our functions are up to date and match the bindings.
func FuzzL1InfoAgainstContract(f *testing.F) {
75
	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, hash []byte, seqNumber uint64, batcherHash []byte, l1FeeOverhead []byte, l1FeeScalar []byte) {
76 77 78 79 80 81
		expected := L1BlockInfo{
			Number:         number,
			Time:           time,
			BaseFee:        BytesToBigInt(baseFee),
			BlockHash:      common.BytesToHash(hash),
			SequenceNumber: seqNumber,
82 83 84
			BatcherAddr:    common.BytesToAddress(batcherHash),
			L1FeeOverhead:  eth.Bytes32(common.BytesToHash(l1FeeOverhead)),
			L1FeeScalar:    eth.Bytes32(common.BytesToHash(l1FeeScalar)),
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
		}

		// Setup opts
		opts.GasPrice = big.NewInt(100)
		opts.GasLimit = 100_000
		opts.NoSend = true
		opts.Nonce = common.Big0
		// Create the SetL1BlockValues transaction
		tx, err := l1BlockInfoContract.SetL1BlockValues(
			opts,
			number,
			time,
			BytesToBigInt(baseFee),
			common.BytesToHash(hash),
			seqNumber,
100 101 102
			common.BytesToAddress(batcherHash).Hash(),
			common.BytesToHash(l1FeeOverhead).Big(),
			common.BytesToHash(l1FeeScalar).Big(),
103 104 105 106 107 108 109 110 111 112 113 114
		)
		if err != nil {
			t.Fatalf("Failed to create the transaction: %v", err)
		}

		// Check that our encoder produces the same value and that we
		// can decode the contract values exactly
		enc, err := expected.MarshalBinary()
		if err != nil {
			t.Fatalf("Failed to marshal binary: %v", err)
		}
		if !bytes.Equal(enc, tx.Data()) {
115 116
			t.Logf("encoded  %x", enc)
			t.Logf("expected %x", tx.Data())
117 118 119 120 121 122 123 124 125
			t.Fatalf("Custom marshal does not match contract bindings")
		}

		var actual L1BlockInfo
		err = actual.UnmarshalBinary(tx.Data())
		if err != nil {
			t.Fatalf("Failed to unmarshal binary: %v", err)
		}

126
		if !cmp.Equal(expected, actual, cmp.Comparer(testutils.BigEqual)) {
127 128 129 130 131 132
			t.Fatalf("The data did not round trip correctly. expected: %v. actual: %v", expected, actual)
		}

	})
}

133 134 135 136 137 138 139 140 141 142 143
// Standard ABI types copied from golang ABI tests
var (
	Uint256Type, _ = abi.NewType("uint256", "", nil)
	Uint64Type, _  = abi.NewType("uint64", "", nil)
	BytesType, _   = abi.NewType("bytes", "", nil)
	BoolType, _    = abi.NewType("bool", "", nil)
	AddressType, _ = abi.NewType("address", "", nil)
)

// EncodeDepositOpaqueDataV0 performs ABI encoding to create the opaque data field of the deposit event.
func EncodeDepositOpaqueDataV0(t *testing.T, mint *big.Int, value *big.Int, gasLimit uint64, isCreation bool, data []byte) []byte {
144
	t.Helper()
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
	// in OptimismPortal.sol:
	// bytes memory opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data);
	// Geth does not support abi.encodePacked, so we emulate it here by slicing of the padding from the individual elements
	// See https://github.com/ethereum/go-ethereum/issues/22257
	// And https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#non-standard-packed-mode

	var out []byte

	v, err := abi.Arguments{{Name: "msg.value", Type: Uint256Type}}.Pack(mint)
	require.NoError(t, err)
	out = append(out, v...)

	v, err = abi.Arguments{{Name: "_value", Type: Uint256Type}}.Pack(value)
	require.NoError(t, err)
	out = append(out, v...)

	v, err = abi.Arguments{{Name: "_gasLimit", Type: Uint64Type}}.Pack(gasLimit)
	require.NoError(t, err)
	out = append(out, v[32-8:]...) // 8 bytes only with abi.encodePacked

	v, err = abi.Arguments{{Name: "_isCreation", Type: BoolType}}.Pack(isCreation)
	require.NoError(t, err)
	out = append(out, v[32-1:]...) // 1 byte only with abi.encodePacked

	// no slice header, just the raw data with abi.encodePacked
	out = append(out, data...)

	return out
}

175
// FuzzUnmarshallLogEvent runs a deposit event through the EVM and checks that output of the abigen parsing matches
176
// what was inputted and what we parsed during the UnmarshalDepositLogEvent function (which turns it into a deposit tx)
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
// The purpose is to check that we can never create a transaction that emits a log that we cannot parse as well
// as ensuring that our custom marshalling matches abigen.
func FuzzUnmarshallLogEvent(f *testing.F) {
	b := func(i int64) []byte {
		return big.NewInt(i).Bytes()
	}
	type setup struct {
		to         common.Address
		mint       int64
		value      int64
		gasLimit   uint64
		data       string
		isCreation bool
	}
	cases := []setup{
		{
			mint:     100,
			value:    50,
			gasLimit: 100000,
		},
	}
	for _, c := range cases {
		f.Add(c.to.Bytes(), b(c.mint), b(c.value), []byte(c.data), c.gasLimit, c.isCreation)
	}

	f.Fuzz(func(t *testing.T, _to, _mint, _value, data []byte, l2GasLimit uint64, isCreation bool) {
		to := common.BytesToAddress(_to)
		mint := BytesToBigInt(_mint)
		value := BytesToBigInt(_value)

		// Setup opts
		opts.Value = mint
		opts.GasPrice = common.Big2
		opts.GasLimit = 500_000
		opts.NoSend = true
		opts.Nonce = common.Big0
		// Create the deposit transaction
		tx, err := portalContract.DepositTransaction(opts, to, value, l2GasLimit, isCreation, data)
		if err != nil {
			t.Fatal(err)
		}

		state, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
		if err != nil {
			t.Fatal(err)
		}
		state.SetBalance(from, BytesToBigInt([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}))
224
		state.SetCode(addr, common.FromHex(bindings.OptimismPortalDeployedBin))
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
		_, err = state.Commit(false)
		if err != nil {
			t.Fatal(err)
		}

		cfg := runtime.Config{
			Origin:   from,
			Value:    tx.Value(),
			State:    state,
			GasLimit: opts.GasLimit,
		}

		_, _, err = runtime.Call(addr, tx.Data(), &cfg)
		logs := state.Logs()
		if err == nil && len(logs) != 1 {
			t.Fatal("No logs or error after execution")
		} else if err != nil {
			return
		}

		// Test that our custom parsing matches the ABI parsing
		depositEvent, err := portalContract.ParseTransactionDeposited(*(logs[0]))
		if err != nil {
			t.Fatalf("Could not parse log that was emitted by the deposit contract: %v", err)
		}
		depositEvent.Raw = types.Log{} // Clear out the log

		// Verify that is passes our custom unmarshalling logic
253
		dep, err := UnmarshalDepositLogEvent(logs[0])
254 255 256
		if err != nil {
			t.Fatalf("Could not unmarshal log that was emitted by the deposit contract: %v", err)
		}
257 258 259 260 261
		depMint := common.Big0
		if dep.Mint != nil {
			depMint = dep.Mint
		}
		opaqueData := EncodeDepositOpaqueDataV0(t, depMint, dep.Value, dep.Gas, dep.To == nil, dep.Data)
262

263
		reconstructed := &bindings.OptimismPortalTransactionDeposited{
264
			From:       dep.From,
265 266
			Version:    common.Big0,
			OpaqueData: opaqueData,
267 268 269 270 271 272
			Raw:        types.Log{},
		}
		if dep.To != nil {
			reconstructed.To = *dep.To
		}

273
		if !cmp.Equal(depositEvent, reconstructed, cmp.Comparer(testutils.BigEqual)) {
274 275 276
			t.Fatalf("The deposit tx did not match. tx: %v. actual: %v", reconstructed, depositEvent)
		}

277 278
		opaqueData = EncodeDepositOpaqueDataV0(t, mint, value, l2GasLimit, isCreation, data)

279
		inputArgs := &bindings.OptimismPortalTransactionDeposited{
280 281
			From:       from,
			To:         to,
282 283
			Version:    common.Big0,
			OpaqueData: opaqueData,
284 285
			Raw:        types.Log{},
		}
286
		if !cmp.Equal(depositEvent, inputArgs, cmp.Comparer(testutils.BigEqual)) {
287 288 289 290
			t.Fatalf("The input args did not match. input: %v. actual: %v", inputArgs, depositEvent)
		}
	})
}