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

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

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

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

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

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

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

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

	})
}

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
// 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 {
			t.Fatalf("Failed to marshal binary: %v", err)
		}
		var out L1BlockInfo
		err = out.unmarshalBinaryEcotone(enc)
		if err != nil {
			t.Fatalf("Failed to unmarshal binary: %v", err)
		}
		if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
			t.Fatalf("The data did not round trip correctly. in: %v. out: %v", in, out)
		}

	})
}

// 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) {
104 105 106
	l1BlockInfoContract, err := bindings.NewL1Block(common.Address{0x42, 0xff}, nil)
	require.NoError(f, err)

107
	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, hash []byte, seqNumber uint64, batcherHash []byte, l1FeeOverhead []byte, l1FeeScalar []byte) {
108 109 110 111 112 113
		expected := L1BlockInfo{
			Number:         number,
			Time:           time,
			BaseFee:        BytesToBigInt(baseFee),
			BlockHash:      common.BytesToHash(hash),
			SequenceNumber: seqNumber,
114 115 116
			BatcherAddr:    common.BytesToAddress(batcherHash),
			L1FeeOverhead:  eth.Bytes32(common.BytesToHash(l1FeeOverhead)),
			L1FeeScalar:    eth.Bytes32(common.BytesToHash(l1FeeScalar)),
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
		}

		// 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,
132
			eth.AddressAsLeftPaddedHash(common.BytesToAddress(batcherHash)),
133 134
			common.BytesToHash(l1FeeOverhead).Big(),
			common.BytesToHash(l1FeeScalar).Big(),
135 136 137 138 139 140 141
		)
		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
142
		enc, err := expected.marshalBinaryBedrock()
143 144 145 146
		if err != nil {
			t.Fatalf("Failed to marshal binary: %v", err)
		}
		if !bytes.Equal(enc, tx.Data()) {
147 148
			t.Logf("encoded  %x", enc)
			t.Logf("expected %x", tx.Data())
149 150 151 152
			t.Fatalf("Custom marshal does not match contract bindings")
		}

		var actual L1BlockInfo
153
		err = actual.unmarshalBinaryBedrock(tx.Data())
154 155 156 157
		if err != nil {
			t.Fatalf("Failed to unmarshal binary: %v", err)
		}

158
		if !cmp.Equal(expected, actual, cmp.Comparer(testutils.BigEqual)) {
159 160 161 162 163 164
			t.Fatalf("The data did not round trip correctly. expected: %v. actual: %v", expected, actual)
		}

	})
}

165 166 167 168 169 170 171 172 173 174 175
// 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 {
176
	t.Helper()
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
	// 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
}

207
// FuzzUnmarshallLogEvent runs a deposit event through the EVM and checks that output of the abigen parsing matches
208
// what was inputted and what we parsed during the UnmarshalDepositLogEvent function (which turns it into a deposit tx)
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
// 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)
	}

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
	// Set the EVM state up once to fuzz against
	state, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
	require.NoError(f, err)
	state.SetBalance(from, uint256.MustFromBig(BytesToBigInt([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff})))
	_, 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)

251 252 253 254 255 256 257 258 259 260 261 262 263
	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)
264
		require.NoError(t, err)
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288

		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
289
		dep, err := UnmarshalDepositLogEvent(logs[0])
290 291 292
		if err != nil {
			t.Fatalf("Could not unmarshal log that was emitted by the deposit contract: %v", err)
		}
293 294 295 296 297
		depMint := common.Big0
		if dep.Mint != nil {
			depMint = dep.Mint
		}
		opaqueData := EncodeDepositOpaqueDataV0(t, depMint, dep.Value, dep.Gas, dep.To == nil, dep.Data)
298

299
		reconstructed := &bindings.OptimismPortalTransactionDeposited{
300
			From:       dep.From,
301 302
			Version:    common.Big0,
			OpaqueData: opaqueData,
303 304 305 306 307 308
			Raw:        types.Log{},
		}
		if dep.To != nil {
			reconstructed.To = *dep.To
		}

309
		if !cmp.Equal(depositEvent, reconstructed, cmp.Comparer(testutils.BigEqual)) {
310 311 312
			t.Fatalf("The deposit tx did not match. tx: %v. actual: %v", reconstructed, depositEvent)
		}

313 314
		opaqueData = EncodeDepositOpaqueDataV0(t, mint, value, l2GasLimit, isCreation, data)

315
		inputArgs := &bindings.OptimismPortalTransactionDeposited{
316 317
			From:       from,
			To:         to,
318 319
			Version:    common.Big0,
			OpaqueData: opaqueData,
320 321
			Raw:        types.Log{},
		}
322
		if !cmp.Equal(depositEvent, inputArgs, cmp.Comparer(testutils.BigEqual)) {
323 324 325 326
			t.Fatalf("The input args did not match. input: %v. actual: %v", inputArgs, depositEvent)
		}
	})
}