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

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

8
	"github.com/ethereum/go-ethereum/core/tracing"
9
	"github.com/ethereum/go-ethereum/triedb"
10
	"github.com/google/go-cmp/cmp"
11
	"github.com/holiman/uint256"
12 13
	"github.com/stretchr/testify/require"

14
	"github.com/ethereum/go-ethereum/accounts/abi"
15 16 17 18 19 20 21
	"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"
22

23
	"github.com/ethereum-optimism/optimism/op-node/bindings"
24
	"github.com/ethereum-optimism/optimism/op-service/eth"
Sabnock01's avatar
Sabnock01 committed
25
	"github.com/ethereum-optimism/optimism/op-service/testutils"
26 27 28
)

var (
29 30 31
	pk, _   = crypto.GenerateKey()
	opts, _ = bind.NewKeyedTransactorWithChainID(pk, common.Big1)
	from    = crypto.PubkeyToAddress(pk.PublicKey)
32 33 34 35 36 37 38 39 40 41 42 43 44 45
)

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))
}

46 47
// FuzzL1InfoBedrockRoundTrip checks that our Bedrock l1 info encoder round trips properly
func FuzzL1InfoBedrockRoundTrip(f *testing.F) {
48 49 50 51 52 53 54 55
	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,
		}
56
		enc, err := in.marshalBinaryBedrock()
57 58 59 60
		if err != nil {
			t.Fatalf("Failed to marshal binary: %v", err)
		}
		var out L1BlockInfo
61
		err = out.unmarshalBinaryBedrock(enc)
62 63 64
		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
			t.Fatalf("The data did not round trip correctly. in: %v. out: %v", in, out)
		}

	})
}

72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
// FuzzL1InfoEcotoneRoundTrip checks that our Ecotone encoder round trips properly
func FuzzL1InfoEcotoneRoundTrip(f *testing.F) {
	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, blobBaseFee, hash []byte, seqNumber uint64, baseFeeScalar, blobBaseFeeScalar uint32) {
		in := L1BlockInfo{
			Number:            number,
			Time:              time,
			BaseFee:           BytesToBigInt(baseFee),
			BlockHash:         common.BytesToHash(hash),
			SequenceNumber:    seqNumber,
			BlobBaseFee:       BytesToBigInt(blobBaseFee),
			BaseFeeScalar:     baseFeeScalar,
			BlobBaseFeeScalar: blobBaseFeeScalar,
		}
		enc, err := in.marshalBinaryEcotone()
		if err != nil {
87
			t.Fatalf("Failed to marshal Ecotone binary: %v", err)
88 89 90 91
		}
		var out L1BlockInfo
		err = out.unmarshalBinaryEcotone(enc)
		if err != nil {
92
			t.Fatalf("Failed to unmarshal Ecotone binary: %v", err)
93 94
		}
		if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
95 96
			t.Fatalf("The Ecotone data did not round trip correctly. in: %v. out: %v", in, out)
		}
97
		enc, err = in.marshalBinaryInterop()
98
		if err != nil {
99
			t.Fatalf("Failed to marshal Interop binary: %v", err)
100
		}
101
		err = out.unmarshalBinaryInterop(enc)
102
		if err != nil {
103
			t.Fatalf("Failed to unmarshal Interop binary: %v", err)
104 105
		}
		if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
106
			t.Fatalf("The Interop data did not round trip correctly. in: %v. out: %v", in, out)
107 108 109 110 111 112 113 114 115 116
		}

	})
}

// FuzzL1InfoAgainstContract checks the custom Bedrock L1 Info marshalling functions against the
// setL1BlockValues contract bindings to ensure that our functions are up to date and match the
// bindings. Note that we don't test setL1BlockValuesEcotone since it accepts only custom packed
// calldata and cannot be invoked using the generated bindings.
func FuzzL1InfoBedrockAgainstContract(f *testing.F) {
117 118 119
	l1BlockInfoContract, err := bindings.NewL1Block(common.Address{0x42, 0xff}, nil)
	require.NoError(f, err)

120
	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, hash []byte, seqNumber uint64, batcherHash []byte, l1FeeOverhead []byte, l1FeeScalar []byte) {
121 122 123 124 125 126
		expected := L1BlockInfo{
			Number:         number,
			Time:           time,
			BaseFee:        BytesToBigInt(baseFee),
			BlockHash:      common.BytesToHash(hash),
			SequenceNumber: seqNumber,
127 128 129
			BatcherAddr:    common.BytesToAddress(batcherHash),
			L1FeeOverhead:  eth.Bytes32(common.BytesToHash(l1FeeOverhead)),
			L1FeeScalar:    eth.Bytes32(common.BytesToHash(l1FeeScalar)),
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
		}

		// 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,
145
			eth.AddressAsLeftPaddedHash(common.BytesToAddress(batcherHash)),
146 147
			common.BytesToHash(l1FeeOverhead).Big(),
			common.BytesToHash(l1FeeScalar).Big(),
148 149 150 151 152 153 154
		)
		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
155
		enc, err := expected.marshalBinaryBedrock()
156 157 158 159
		if err != nil {
			t.Fatalf("Failed to marshal binary: %v", err)
		}
		if !bytes.Equal(enc, tx.Data()) {
160 161
			t.Logf("encoded  %x", enc)
			t.Logf("expected %x", tx.Data())
162 163 164 165
			t.Fatalf("Custom marshal does not match contract bindings")
		}

		var actual L1BlockInfo
166
		err = actual.unmarshalBinaryBedrock(tx.Data())
167 168 169 170
		if err != nil {
			t.Fatalf("Failed to unmarshal binary: %v", err)
		}

171
		if !cmp.Equal(expected, actual, cmp.Comparer(testutils.BigEqual)) {
172 173 174 175 176 177
			t.Fatalf("The data did not round trip correctly. expected: %v. actual: %v", expected, actual)
		}

	})
}

178 179 180 181 182 183 184 185 186 187 188
// 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 {
189
	t.Helper()
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
	// 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
}

220
// FuzzUnmarshallLogEvent runs a deposit event through the EVM and checks that output of the abigen parsing matches
221
// what was inputted and what we parsed during the UnmarshalDepositLogEvent function (which turns it into a deposit tx)
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
// 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)
	}

247
	// Set the EVM state up once to fuzz against
248
	state, err := state.New(common.Hash{}, state.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil))
249
	require.NoError(f, err)
250
	state.SetBalance(from, uint256.MustFromBig(BytesToBigInt([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff})), tracing.BalanceChangeUnspecified)
251 252 253 254 255 256 257 258 259 260 261 262 263
	_, addr, _, err := runtime.Create(common.FromHex(bindings.OptimismPortalMetaData.Bin), &runtime.Config{
		Origin:   from,
		State:    state,
		GasLimit: 20_000_000,
	})
	require.NoError(f, err)

	_, err = state.Commit(0, false)
	require.NoError(f, err)

	portalContract, err := bindings.NewOptimismPortal(addr, nil)
	require.NoError(f, err)

264 265 266 267 268 269 270 271 272 273 274 275 276
	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)
277
		require.NoError(t, err)
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301

		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
302
		dep, err := UnmarshalDepositLogEvent(logs[0])
303 304 305
		if err != nil {
			t.Fatalf("Could not unmarshal log that was emitted by the deposit contract: %v", err)
		}
306 307 308 309 310
		depMint := common.Big0
		if dep.Mint != nil {
			depMint = dep.Mint
		}
		opaqueData := EncodeDepositOpaqueDataV0(t, depMint, dep.Value, dep.Gas, dep.To == nil, dep.Data)
311

312
		reconstructed := &bindings.OptimismPortalTransactionDeposited{
313
			From:       dep.From,
314 315
			Version:    common.Big0,
			OpaqueData: opaqueData,
316 317 318 319 320 321
			Raw:        types.Log{},
		}
		if dep.To != nil {
			reconstructed.To = *dep.To
		}

322
		if !cmp.Equal(depositEvent, reconstructed, cmp.Comparer(testutils.BigEqual)) {
323 324 325
			t.Fatalf("The deposit tx did not match. tx: %v. actual: %v", reconstructed, depositEvent)
		}

326 327
		opaqueData = EncodeDepositOpaqueDataV0(t, mint, value, l2GasLimit, isCreation, data)

328
		inputArgs := &bindings.OptimismPortalTransactionDeposited{
329 330
			From:       from,
			To:         to,
331 332
			Version:    common.Big0,
			OpaqueData: opaqueData,
333 334
			Raw:        types.Log{},
		}
335
		if !cmp.Equal(depositEvent, inputArgs, cmp.Comparer(testutils.BigEqual)) {
336 337 338 339
			t.Fatalf("The input args did not match. input: %v. actual: %v", inputArgs, depositEvent)
		}
	})
}