github.com/ethereum-optimism/optimism@v1.7.2/op-node/rollup/derive/fuzz_parsers_test.go (about)

     1  package derive
     2  
     3  import (
     4  	"bytes"
     5  	"math/big"
     6  	"testing"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/ethereum/go-ethereum/accounts/abi"
    12  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    13  	"github.com/ethereum/go-ethereum/common"
    14  	"github.com/ethereum/go-ethereum/core/rawdb"
    15  	"github.com/ethereum/go-ethereum/core/state"
    16  	"github.com/ethereum/go-ethereum/core/types"
    17  	"github.com/ethereum/go-ethereum/core/vm/runtime"
    18  	"github.com/ethereum/go-ethereum/crypto"
    19  
    20  	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
    21  	"github.com/ethereum-optimism/optimism/op-service/eth"
    22  	"github.com/ethereum-optimism/optimism/op-service/testutils"
    23  )
    24  
    25  var (
    26  	pk, _                  = crypto.GenerateKey()
    27  	addr                   = common.Address{0x42, 0xff}
    28  	opts, _                = bind.NewKeyedTransactorWithChainID(pk, common.Big1)
    29  	from                   = crypto.PubkeyToAddress(pk.PublicKey)
    30  	portalContract, _      = bindings.NewOptimismPortal(addr, nil)
    31  	l1BlockInfoContract, _ = bindings.NewL1Block(addr, nil)
    32  )
    33  
    34  func cap_byte_slice(b []byte, c int) []byte {
    35  	if len(b) <= c {
    36  		return b
    37  	} else {
    38  		return b[:c]
    39  	}
    40  }
    41  
    42  func BytesToBigInt(b []byte) *big.Int {
    43  	return new(big.Int).SetBytes(cap_byte_slice(b, 32))
    44  }
    45  
    46  // FuzzL1InfoBedrockRoundTrip checks that our Bedrock l1 info encoder round trips properly
    47  func FuzzL1InfoBedrockRoundTrip(f *testing.F) {
    48  	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, hash []byte, seqNumber uint64) {
    49  		in := L1BlockInfo{
    50  			Number:         number,
    51  			Time:           time,
    52  			BaseFee:        BytesToBigInt(baseFee),
    53  			BlockHash:      common.BytesToHash(hash),
    54  			SequenceNumber: seqNumber,
    55  		}
    56  		enc, err := in.marshalBinaryBedrock()
    57  		if err != nil {
    58  			t.Fatalf("Failed to marshal binary: %v", err)
    59  		}
    60  		var out L1BlockInfo
    61  		err = out.unmarshalBinaryBedrock(enc)
    62  		if err != nil {
    63  			t.Fatalf("Failed to unmarshal binary: %v", err)
    64  		}
    65  		if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
    66  			t.Fatalf("The data did not round trip correctly. in: %v. out: %v", in, out)
    67  		}
    68  
    69  	})
    70  }
    71  
    72  // FuzzL1InfoEcotoneRoundTrip checks that our Ecotone encoder round trips properly
    73  func FuzzL1InfoEcotoneRoundTrip(f *testing.F) {
    74  	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, blobBaseFee, hash []byte, seqNumber uint64, baseFeeScalar, blobBaseFeeScalar uint32) {
    75  		in := L1BlockInfo{
    76  			Number:            number,
    77  			Time:              time,
    78  			BaseFee:           BytesToBigInt(baseFee),
    79  			BlockHash:         common.BytesToHash(hash),
    80  			SequenceNumber:    seqNumber,
    81  			BlobBaseFee:       BytesToBigInt(blobBaseFee),
    82  			BaseFeeScalar:     baseFeeScalar,
    83  			BlobBaseFeeScalar: blobBaseFeeScalar,
    84  		}
    85  		enc, err := in.marshalBinaryEcotone()
    86  		if err != nil {
    87  			t.Fatalf("Failed to marshal binary: %v", err)
    88  		}
    89  		var out L1BlockInfo
    90  		err = out.unmarshalBinaryEcotone(enc)
    91  		if err != nil {
    92  			t.Fatalf("Failed to unmarshal binary: %v", err)
    93  		}
    94  		if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) {
    95  			t.Fatalf("The data did not round trip correctly. in: %v. out: %v", in, out)
    96  		}
    97  
    98  	})
    99  }
   100  
   101  // FuzzL1InfoAgainstContract checks the custom Bedrock L1 Info marshalling functions against the
   102  // setL1BlockValues contract bindings to ensure that our functions are up to date and match the
   103  // bindings. Note that we don't test setL1BlockValuesEcotone since it accepts only custom packed
   104  // calldata and cannot be invoked using the generated bindings.
   105  func FuzzL1InfoBedrockAgainstContract(f *testing.F) {
   106  	f.Fuzz(func(t *testing.T, number, time uint64, baseFee, hash []byte, seqNumber uint64, batcherHash []byte, l1FeeOverhead []byte, l1FeeScalar []byte) {
   107  		expected := L1BlockInfo{
   108  			Number:         number,
   109  			Time:           time,
   110  			BaseFee:        BytesToBigInt(baseFee),
   111  			BlockHash:      common.BytesToHash(hash),
   112  			SequenceNumber: seqNumber,
   113  			BatcherAddr:    common.BytesToAddress(batcherHash),
   114  			L1FeeOverhead:  eth.Bytes32(common.BytesToHash(l1FeeOverhead)),
   115  			L1FeeScalar:    eth.Bytes32(common.BytesToHash(l1FeeScalar)),
   116  		}
   117  
   118  		// Setup opts
   119  		opts.GasPrice = big.NewInt(100)
   120  		opts.GasLimit = 100_000
   121  		opts.NoSend = true
   122  		opts.Nonce = common.Big0
   123  		// Create the SetL1BlockValues transaction
   124  		tx, err := l1BlockInfoContract.SetL1BlockValues(
   125  			opts,
   126  			number,
   127  			time,
   128  			BytesToBigInt(baseFee),
   129  			common.BytesToHash(hash),
   130  			seqNumber,
   131  			eth.AddressAsLeftPaddedHash(common.BytesToAddress(batcherHash)),
   132  			common.BytesToHash(l1FeeOverhead).Big(),
   133  			common.BytesToHash(l1FeeScalar).Big(),
   134  		)
   135  		if err != nil {
   136  			t.Fatalf("Failed to create the transaction: %v", err)
   137  		}
   138  
   139  		// Check that our encoder produces the same value and that we
   140  		// can decode the contract values exactly
   141  		enc, err := expected.marshalBinaryBedrock()
   142  		if err != nil {
   143  			t.Fatalf("Failed to marshal binary: %v", err)
   144  		}
   145  		if !bytes.Equal(enc, tx.Data()) {
   146  			t.Logf("encoded  %x", enc)
   147  			t.Logf("expected %x", tx.Data())
   148  			t.Fatalf("Custom marshal does not match contract bindings")
   149  		}
   150  
   151  		var actual L1BlockInfo
   152  		err = actual.unmarshalBinaryBedrock(tx.Data())
   153  		if err != nil {
   154  			t.Fatalf("Failed to unmarshal binary: %v", err)
   155  		}
   156  
   157  		if !cmp.Equal(expected, actual, cmp.Comparer(testutils.BigEqual)) {
   158  			t.Fatalf("The data did not round trip correctly. expected: %v. actual: %v", expected, actual)
   159  		}
   160  
   161  	})
   162  }
   163  
   164  // Standard ABI types copied from golang ABI tests
   165  var (
   166  	Uint256Type, _ = abi.NewType("uint256", "", nil)
   167  	Uint64Type, _  = abi.NewType("uint64", "", nil)
   168  	BytesType, _   = abi.NewType("bytes", "", nil)
   169  	BoolType, _    = abi.NewType("bool", "", nil)
   170  	AddressType, _ = abi.NewType("address", "", nil)
   171  )
   172  
   173  // EncodeDepositOpaqueDataV0 performs ABI encoding to create the opaque data field of the deposit event.
   174  func EncodeDepositOpaqueDataV0(t *testing.T, mint *big.Int, value *big.Int, gasLimit uint64, isCreation bool, data []byte) []byte {
   175  	t.Helper()
   176  	// in OptimismPortal.sol:
   177  	// bytes memory opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data);
   178  	// Geth does not support abi.encodePacked, so we emulate it here by slicing of the padding from the individual elements
   179  	// See https://github.com/ethereum/go-ethereum/issues/22257
   180  	// And https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#non-standard-packed-mode
   181  
   182  	var out []byte
   183  
   184  	v, err := abi.Arguments{{Name: "msg.value", Type: Uint256Type}}.Pack(mint)
   185  	require.NoError(t, err)
   186  	out = append(out, v...)
   187  
   188  	v, err = abi.Arguments{{Name: "_value", Type: Uint256Type}}.Pack(value)
   189  	require.NoError(t, err)
   190  	out = append(out, v...)
   191  
   192  	v, err = abi.Arguments{{Name: "_gasLimit", Type: Uint64Type}}.Pack(gasLimit)
   193  	require.NoError(t, err)
   194  	out = append(out, v[32-8:]...) // 8 bytes only with abi.encodePacked
   195  
   196  	v, err = abi.Arguments{{Name: "_isCreation", Type: BoolType}}.Pack(isCreation)
   197  	require.NoError(t, err)
   198  	out = append(out, v[32-1:]...) // 1 byte only with abi.encodePacked
   199  
   200  	// no slice header, just the raw data with abi.encodePacked
   201  	out = append(out, data...)
   202  
   203  	return out
   204  }
   205  
   206  // FuzzUnmarshallLogEvent runs a deposit event through the EVM and checks that output of the abigen parsing matches
   207  // what was inputted and what we parsed during the UnmarshalDepositLogEvent function (which turns it into a deposit tx)
   208  // The purpose is to check that we can never create a transaction that emits a log that we cannot parse as well
   209  // as ensuring that our custom marshalling matches abigen.
   210  func FuzzUnmarshallLogEvent(f *testing.F) {
   211  	b := func(i int64) []byte {
   212  		return big.NewInt(i).Bytes()
   213  	}
   214  	type setup struct {
   215  		to         common.Address
   216  		mint       int64
   217  		value      int64
   218  		gasLimit   uint64
   219  		data       string
   220  		isCreation bool
   221  	}
   222  	cases := []setup{
   223  		{
   224  			mint:     100,
   225  			value:    50,
   226  			gasLimit: 100000,
   227  		},
   228  	}
   229  	for _, c := range cases {
   230  		f.Add(c.to.Bytes(), b(c.mint), b(c.value), []byte(c.data), c.gasLimit, c.isCreation)
   231  	}
   232  
   233  	f.Fuzz(func(t *testing.T, _to, _mint, _value, data []byte, l2GasLimit uint64, isCreation bool) {
   234  		to := common.BytesToAddress(_to)
   235  		mint := BytesToBigInt(_mint)
   236  		value := BytesToBigInt(_value)
   237  
   238  		// Setup opts
   239  		opts.Value = mint
   240  		opts.GasPrice = common.Big2
   241  		opts.GasLimit = 500_000
   242  		opts.NoSend = true
   243  		opts.Nonce = common.Big0
   244  		// Create the deposit transaction
   245  		tx, err := portalContract.DepositTransaction(opts, to, value, l2GasLimit, isCreation, data)
   246  		if err != nil {
   247  			t.Fatal(err)
   248  		}
   249  
   250  		state, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
   251  		if err != nil {
   252  			t.Fatal(err)
   253  		}
   254  		state.SetBalance(from, BytesToBigInt([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}))
   255  		state.SetCode(addr, common.FromHex(bindings.OptimismPortalDeployedBin))
   256  		_, err = state.Commit(0, false)
   257  		if err != nil {
   258  			t.Fatal(err)
   259  		}
   260  
   261  		cfg := runtime.Config{
   262  			Origin:   from,
   263  			Value:    tx.Value(),
   264  			State:    state,
   265  			GasLimit: opts.GasLimit,
   266  		}
   267  
   268  		_, _, err = runtime.Call(addr, tx.Data(), &cfg)
   269  		logs := state.Logs()
   270  		if err == nil && len(logs) != 1 {
   271  			t.Fatal("No logs or error after execution")
   272  		} else if err != nil {
   273  			return
   274  		}
   275  
   276  		// Test that our custom parsing matches the ABI parsing
   277  		depositEvent, err := portalContract.ParseTransactionDeposited(*(logs[0]))
   278  		if err != nil {
   279  			t.Fatalf("Could not parse log that was emitted by the deposit contract: %v", err)
   280  		}
   281  		depositEvent.Raw = types.Log{} // Clear out the log
   282  
   283  		// Verify that is passes our custom unmarshalling logic
   284  		dep, err := UnmarshalDepositLogEvent(logs[0])
   285  		if err != nil {
   286  			t.Fatalf("Could not unmarshal log that was emitted by the deposit contract: %v", err)
   287  		}
   288  		depMint := common.Big0
   289  		if dep.Mint != nil {
   290  			depMint = dep.Mint
   291  		}
   292  		opaqueData := EncodeDepositOpaqueDataV0(t, depMint, dep.Value, dep.Gas, dep.To == nil, dep.Data)
   293  
   294  		reconstructed := &bindings.OptimismPortalTransactionDeposited{
   295  			From:       dep.From,
   296  			Version:    common.Big0,
   297  			OpaqueData: opaqueData,
   298  			Raw:        types.Log{},
   299  		}
   300  		if dep.To != nil {
   301  			reconstructed.To = *dep.To
   302  		}
   303  
   304  		if !cmp.Equal(depositEvent, reconstructed, cmp.Comparer(testutils.BigEqual)) {
   305  			t.Fatalf("The deposit tx did not match. tx: %v. actual: %v", reconstructed, depositEvent)
   306  		}
   307  
   308  		opaqueData = EncodeDepositOpaqueDataV0(t, mint, value, l2GasLimit, isCreation, data)
   309  
   310  		inputArgs := &bindings.OptimismPortalTransactionDeposited{
   311  			From:       from,
   312  			To:         to,
   313  			Version:    common.Big0,
   314  			OpaqueData: opaqueData,
   315  			Raw:        types.Log{},
   316  		}
   317  		if !cmp.Equal(depositEvent, inputArgs, cmp.Comparer(testutils.BigEqual)) {
   318  			t.Fatalf("The input args did not match. input: %v. actual: %v", inputArgs, depositEvent)
   319  		}
   320  	})
   321  }