fuzz_parsers_test.go 10.6 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/google/go-cmp/cmp"
10
	"github.com/holiman/uint256"
11 12
	"github.com/stretchr/testify/require"

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

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

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

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

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

	})
}

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

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

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

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

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

	})
}

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

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

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

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

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

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

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

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

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