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 }