diff --git a/README.md b/README.md index d4299b61..5693923f 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,15 @@ Supersim allows developers to start multiple local evm nodes with one command, a Supersim is a lightweight tool that simulates an interoperable Superchain environment locally. It does not require a complicated devnet setup and is run using cli commands with configuration options that fall back to sensible defaults if they are not specified. Each chain is an instance of [anvil](https://book.getfoundry.sh/reference/anvil/), though future versions may support other local testing tools. ## Getting started -### Installation -TODO +### Running Locally +1. build the binary by running: +``` +go build cmd/main.go +``` +2. start supersim in vanilla mode by running: +``` +./main +``` ## Features ### Vanilla mode diff --git a/anvil/anvil.go b/anvil/anvil.go index a693196f..031bfa87 100644 --- a/anvil/anvil.go +++ b/anvil/anvil.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "math/big" "os" "os/exec" "strconv" @@ -283,7 +284,24 @@ func (a *Anvil) EthSendTransaction(ctx context.Context, tx *types.Transaction) e return a.ethClient.SendTransaction(ctx, tx) } +func (a *Anvil) EthBlockByNumber(ctx context.Context, blockHeight *big.Int) (*types.Block, error) { + return a.ethClient.BlockByNumber(ctx, blockHeight) +} + // subscription API func (a *Anvil) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { return a.ethClient.SubscribeFilterLogs(ctx, q, ch) } + +func (a *Anvil) DebugTraceCall(ctx context.Context, txArgs config.TransactionArgs) (config.TraceCallRaw, error) { + var result config.TraceCallRaw + if err := a.rpcClient.CallContext(ctx, &result, "debug_traceCall", txArgs, "latest", map[string]interface{}{ + "tracer": "callTracer", + "tracerConfig": map[string]interface{}{ + "withLog": true, + }, + }); err != nil { + return config.TraceCallRaw{}, err + } + return result, nil +} diff --git a/config/chain.go b/config/chain.go index 5fab2504..36aca5b8 100644 --- a/config/chain.go +++ b/config/chain.go @@ -3,6 +3,7 @@ package config import ( "context" "fmt" + "math/big" "strings" registry "github.com/ethereum-optimism/superchain-registry/superchain" @@ -12,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" ) @@ -96,6 +98,35 @@ type NetworkConfig struct { L2Configs []ChainConfig } +type TransactionArgs struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Data hexutil.Bytes `json:"data"` + Value *hexutil.Big `json:"value"` +} + +type TraceCallRaw struct { + Error *string `json:"error,omitempty"` + Type string `json:"type"` + From string `json:"from"` + To string `json:"to"` + Value string `json:"value"` + Gas string `json:"gas"` + GasUsed string `json:"gasUsed"` + Input string `json:"input"` + Output string `json:"output"` + Logs []*TraceCallRawLog `json:"logs"` + Calls []TraceCallRaw `json:"calls"` +} + +type TraceCallRawLog struct { + Address common.Address `json:"address"` + Topics []common.Hash `json:"topics"` + Data string `json:"data"` +} + type Chain interface { Name() string Endpoint() string @@ -109,7 +140,10 @@ type Chain interface { EthGetCode(ctx context.Context, account common.Address) ([]byte, error) EthGetLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) EthSendTransaction(ctx context.Context, tx *types.Transaction) error + EthBlockByNumber(ctx context.Context, blockHeight *big.Int) (*types.Block, error) + SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) + DebugTraceCall(ctx context.Context, txArgs TransactionArgs) (TraceCallRaw, error) } // Note: The default secrets config is used everywhere diff --git a/contracts/.gitignore b/contracts/.gitignore index 85198aaa..dc45b573 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -5,6 +5,8 @@ out/ # Ignores development broadcast logs !/broadcast /broadcast/*/31337/ +/broadcast/*/901/ +/broadcast/*/902/ /broadcast/**/dry-run/ # Docs diff --git a/crossl2inbox/crossl2inbox.go b/crossl2inbox/crossl2inbox.go new file mode 100644 index 00000000..d003ac2c --- /dev/null +++ b/crossl2inbox/crossl2inbox.go @@ -0,0 +1,73 @@ +package crossl2inbox + +import ( + _ "embed" + "errors" + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum-optimism/optimism/op-service/sources/batching" + "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + ethTypes "github.com/ethereum/go-ethereum/core/types" +) + +const ( + eventExecutingMessage = "ExecutingMessage" +) + +var ( + ErrEventNotFound = errors.New("event not found") +) + +type ExecutingMessage struct { + MsgHash [32]byte + Identifier Identifier +} + +type Identifier struct { + Origin common.Address + BlockNumber *big.Int + LogIndex *big.Int + Timestamp *big.Int + ChainId *big.Int +} + +type CrossL2Inbox struct { + Contract *batching.BoundContract + Abi abi.ABI +} + +func NewCrossL2Inbox() *CrossL2Inbox { + abi := snapshots.LoadCrossL2InboxABI() + return &CrossL2Inbox{ + Abi: *abi, + Contract: batching.NewBoundContract(abi, predeploys.CrossL2InboxAddr), + } +} + +func (i *CrossL2Inbox) DecodeExecutingMessageLog(l *ethTypes.Log) (*ExecutingMessage, error) { + if l.Address != i.Contract.Addr() { + return nil, nil + } + name, result, err := i.Contract.DecodeEvent(l) + if errors.Is(err, batching.ErrUnknownEvent) { + return nil, fmt.Errorf("%w: %v", ErrEventNotFound, err.Error()) + } else if err != nil { + return nil, fmt.Errorf("failed to decode event: %w", err) + } + if name != eventExecutingMessage { + return nil, nil + } + + msgHash := result.GetBytes32(0) + var messageIdentifier Identifier + result.GetStruct(1, &messageIdentifier) + + return &ExecutingMessage{ + MsgHash: msgHash, + Identifier: messageIdentifier, + }, nil +} diff --git a/opsimulator/opsimulator.go b/opsimulator/opsimulator.go index 7fcc7c96..4fb30900 100644 --- a/opsimulator/opsimulator.go +++ b/opsimulator/opsimulator.go @@ -3,6 +3,7 @@ package opsimulator import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -18,9 +19,15 @@ import ( ophttp "github.com/ethereum-optimism/optimism/op-service/httputil" "github.com/ethereum-optimism/optimism/op-service/tasks" + "github.com/ethereum-optimism/supersim/anvil" "github.com/ethereum-optimism/supersim/config" + "github.com/ethereum-optimism/supersim/crossl2inbox" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" ) @@ -40,6 +47,7 @@ type OpSimulator struct { bgTasks tasks.Group bgTasksCtx context.Context bgTasksCancel context.CancelFunc + clients map[uint64]*anvil.Anvil // One time tasks at startup startupTasks tasks.Group @@ -52,7 +60,13 @@ type OpSimulator struct { stopped atomic.Bool } -func New(log log.Logger, port uint64, l1Chain, l2Chain config.Chain, l2Config *config.L2Config) *OpSimulator { +type SimulatedLog struct { + Address common.Address + Topics []common.Hash + Data []byte +} + +func New(log log.Logger, port uint64, l1Chain, l2Chain config.Chain, l2Config *config.L2Config, clients map[uint64]*anvil.Anvil) *OpSimulator { bgTasksCtx, bgTasksCancel := context.WithCancel(context.Background()) startupTasksCtx, startupTasksCancel := context.WithCancel(context.Background()) @@ -78,6 +92,7 @@ func New(log log.Logger, port uint64, l1Chain, l2Chain config.Chain, l2Config *c log.Error("startup task failed", err) }, }, + clients: clients, } } @@ -88,7 +103,7 @@ func (opSim *OpSimulator) Start(ctx context.Context) error { } mux := http.NewServeMux() - mux.Handle("/", handler(proxy)) + mux.Handle("/", opSim.handler(proxy, ctx)) hs, err := ophttp.StartHTTPServer(net.JoinHostPort(host, fmt.Sprintf("%d", opSim.port)), mux) if err != nil { @@ -166,7 +181,7 @@ func (opSim *OpSimulator) startBackgroundTasks() { }) } -func handler(proxy *httputil.ReverseProxy) http.HandlerFunc { +func (opSim *OpSimulator) handler(proxy *httputil.ReverseProxy, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions { // handle preflight requests @@ -189,11 +204,37 @@ func handler(proxy *httputil.ReverseProxy) http.HandlerFunc { return } - // TODO(https://github.com/ethereum-optimism/supersim/issues/55): support batch txs - for _, msg := range msgs { if msg.Method == "eth_sendRawTransaction" { - checkInteropInvariants() + var params []string + if err := json.Unmarshal(msg.Params, ¶ms); err != nil { + http.Error(w, fmt.Sprintf("bad params sent to eth_sendRawTransaction: %v", err), http.StatusBadRequest) + return + } + if len(params) != 1 { + http.Error(w, "eth_sendRawTransaction request has invalid number of params", http.StatusBadRequest) + return + } + + var data hexutil.Bytes + if err := data.UnmarshalText([]byte(params[0])); err != nil { + http.Error(w, fmt.Sprintf("failed to decode transaction data: %v", err), http.StatusBadRequest) + return + } + + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(data); err != nil { + http.Error(w, fmt.Sprintf("failed to decode transaction data: %v", err), http.StatusBadRequest) + return + } + + err := opSim.checkInteropInvariants(ctx, tx) + if err != nil { + opSim.log.Error(fmt.Sprintf("interop invariants not met: %v", err)) + // TODO (https://github.com/ethereum-optimism/supersim/issues/79) for batch requests write error to individual tx + http.Error(w, fmt.Sprintf("interop invariants not met: %v", err), http.StatusBadRequest) + return + } } } @@ -216,9 +257,120 @@ func (opSim *OpSimulator) AddDependency(chainID uint64, depSet []uint64) error { return nil } -// TODO(https://github.com/ethereum-optimism/supersim/issues/19): add logic for checking that an interop transaction is valid. -func checkInteropInvariants() bool { - return true +func (opSim *OpSimulator) checkInteropInvariants(ctx context.Context, tx *types.Transaction) error { + from, err := getFromAddress(tx) + + if err != nil { + return fmt.Errorf("failed to find sender of transaction: %w", err) + } + + result, err := opSim.l2Chain.DebugTraceCall(ctx, config.TransactionArgs{From: from, To: tx.To(), Gas: hexutil.Uint64(tx.Gas()), GasPrice: (*hexutil.Big)(tx.GasPrice()), Data: tx.Data(), Value: (*hexutil.Big)(tx.Value())}) + if err != nil { + return fmt.Errorf("failed to simulate transaction: %w", err) + } + if result.Error != nil { + return fmt.Errorf("tx trace error: %v", result.Error) + } + simulatedLogs := toSimulatedLogs(result) + + crossL2Inbox := crossl2inbox.NewCrossL2Inbox() + var executingMessages []crossl2inbox.ExecutingMessage + for _, log := range simulatedLogs { + executingMessage, err := crossL2Inbox.DecodeExecutingMessageLog(&log) + if err != nil { + return fmt.Errorf("failed to decode executing messages from transaction logs: %w", err) + } + + if executingMessage != nil { + executingMessages = append(executingMessages, *executingMessage) + } + } + + if len(executingMessages) >= 1 { + for _, executingMessage := range executingMessages { + client := opSim.clients[executingMessage.Identifier.ChainId.Uint64()] + if client == nil { + return fmt.Errorf("no client found for chainid: %v on executing message", executingMessage.Identifier.ChainId) + } + + block, err := client.EthBlockByNumber(ctx, executingMessage.Identifier.BlockNumber) + if err != nil { + return fmt.Errorf("failed to fetch executing message block: %w", err) + } + + if executingMessage.Identifier.Timestamp.Cmp(new(big.Int).SetUint64(block.Time())) != 0 { + return fmt.Errorf("executing message identifier does not match block timestamp: %w", err) + } + + logs, err := client.EthGetLogs( + ctx, + ethereum.FilterQuery{ + Addresses: []common.Address{executingMessage.Identifier.Origin}, + FromBlock: executingMessage.Identifier.BlockNumber, + ToBlock: executingMessage.Identifier.BlockNumber, + }, + ) + if err != nil { + return fmt.Errorf("failed to fetch initiating message logs: %w", err) + } + var initiatingMessageLogs []ethTypes.Log + for _, log := range logs { + logIndex := big.NewInt(int64(log.Index)) + if logIndex.Cmp(executingMessage.Identifier.LogIndex) == 0 { + initiatingMessageLogs = append(initiatingMessageLogs, log) + } + } + + if len(initiatingMessageLogs) == 0 { + return fmt.Errorf("initiating message not found") + } + + // Since we look for a log at a specific index, this should never be more than 1. + if len(initiatingMessageLogs) > 1 { + return fmt.Errorf("unexpected number of initiating messages found: %v", len(initiatingMessageLogs)) + } + + initiatingMsgPayloadHash := crypto.Keccak256Hash(messagePayloadBytes(&initiatingMessageLogs[0])) + if common.BytesToHash(executingMessage.MsgHash[:]).Cmp(initiatingMsgPayloadHash) != 0 { + return fmt.Errorf("executing and initiating message fields are not equal") + } + } + } + + return nil +} + +func toSimulatedLogs(call config.TraceCallRaw) []ethTypes.Log { + var logs []ethTypes.Log + + // Recursively append the logs for each nested call + for _, call := range call.Calls { + logs = append(logs, toSimulatedLogs(call)...) + } + + for _, log := range call.Logs { + logs = append(logs, ethTypes.Log{ + Address: log.Address, + Topics: log.Topics, + Data: common.FromHex(log.Data), + }) + } + + return logs +} + +func messagePayloadBytes(log *ethTypes.Log) []byte { + msg := []byte{} + for _, topic := range log.Topics { + msg = append(msg, topic.Bytes()...) + } + return append(msg, log.Data...) +} + +func getFromAddress(tx *ethTypes.Transaction) (common.Address, error) { + from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx) + + return from, err } func (opSim *OpSimulator) createReverseProxy() (*httputil.ReverseProxy, error) { diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go index a6664f53..8271eaa6 100644 --- a/orchestrator/orchestrator.go +++ b/orchestrator/orchestrator.go @@ -36,7 +36,7 @@ func NewOrchestrator(log log.Logger, networkConfig *config.NetworkConfig) (*Orch l2Anvil := anvil.New(log, &cfg) l2Anvils[cfg.ChainID] = l2Anvil - L2OpSims[cfg.ChainID] = opsimulator.New(log, nextL2Port, l1Anvil, l2Anvil, cfg.L2Config) + L2OpSims[cfg.ChainID] = opsimulator.New(log, nextL2Port, l1Anvil, l2Anvil, cfg.L2Config, l2Anvils) // only increment expected port if it has been specified if nextL2Port > 0 { diff --git a/supersim_test.go b/supersim_test.go index 467c4b60..a7a330e2 100644 --- a/supersim_test.go +++ b/supersim_test.go @@ -3,22 +3,28 @@ package supersim import ( "context" "math/big" + "strings" "sync" "testing" "time" opbindings "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/supersim/bindings" "github.com/ethereum-optimism/supersim/config" + "github.com/ethereum-optimism/supersim/crossl2inbox" "github.com/ethereum-optimism/supersim/hdaccount" "github.com/ethereum-optimism/supersim/opsimulator" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" @@ -29,7 +35,6 @@ import ( const ( anvilClientTimeout = 5 * time.Second emptyCode = "0x" - crossL2InboxAddress = "0x4200000000000000000000000000000000000022" l2toL2CrossDomainMessengerAddress = "0x4200000000000000000000000000000000000023" l1BlockAddress = "0x4200000000000000000000000000000000000015" defaultTestAccountBalance = "0x21e19e0c9bab2400000" @@ -60,6 +65,23 @@ type TestSuite struct { Supersim *Supersim } +type InteropTestSuite struct { + t *testing.T + HdAccountStore *hdaccount.HdAccountStore + + Cfg *config.CLIConfig + Supersim *Supersim + + // Op Simulator for the source chain. + SourceOpSim *opsimulator.OpSimulator + SourceEthClient *ethclient.Client + // Op Simulator for the destination chain. + DestOpSim *opsimulator.OpSimulator + DestEthClient *ethclient.Client + SourceChainID *big.Int + DestChainID *big.Int +} + func createTestSuite(t *testing.T) *TestSuite { cfg := &config.CLIConfig{} // does not run in fork mode testlog := testlog.Logger(t, log.LevelInfo) @@ -90,6 +112,37 @@ func createTestSuite(t *testing.T) *TestSuite { } } +func createInteropTestSuite(t *testing.T) *InteropTestSuite { + testSuite := createTestSuite(t) + + sourceOpSim := testSuite.Supersim.Orchestrator.L2OpSims[config.DefaultNetworkConfig.L2Configs[0].ChainID] + sourceEthClient, _ := ethclient.Dial(sourceOpSim.Endpoint()) + defer sourceEthClient.Close() + + destOpSim := testSuite.Supersim.Orchestrator.L2OpSims[config.DefaultNetworkConfig.L2Configs[1].ChainID] + destEthClient, _ := ethclient.Dial(destOpSim.Endpoint()) + defer destEthClient.Close() + + destChainID := new(big.Int).SetUint64(config.DefaultNetworkConfig.L2Configs[1].ChainID) + sourceChainID := new(big.Int).SetUint64(config.DefaultNetworkConfig.L2Configs[0].ChainID) + + // TODO: fix when we add a wait for ready on the opsim + time.Sleep(3 * time.Second) + + return &InteropTestSuite{ + t: t, + Cfg: testSuite.Cfg, + Supersim: testSuite.Supersim, + HdAccountStore: testSuite.HdAccountStore, + SourceOpSim: sourceOpSim, + SourceEthClient: sourceEthClient, + DestOpSim: destOpSim, + DestEthClient: destEthClient, + SourceChainID: sourceChainID, + DestChainID: destChainID, + } +} + func TestStartup(t *testing.T) { testSuite := createTestSuite(t) @@ -144,7 +197,7 @@ func TestGenesisState(t *testing.T) { defer client.Close() var code string - require.NoError(t, client.CallContext(context.Background(), &code, "eth_getCode", crossL2InboxAddress, "latest")) + require.NoError(t, client.CallContext(context.Background(), &code, "eth_getCode", predeploys.CrossL2Inbox, "latest")) require.NotEqual(t, emptyCode, code, "CrossL2Inbox is not deployed") require.NoError(t, client.CallContext(context.Background(), &code, "eth_getCode", l2toL2CrossDomainMessengerAddress, "latest")) @@ -268,3 +321,231 @@ func TestBatchJsonRpcRequests(t *testing.T) { // require.NotZero(t, uint64(*(elems[1].Result).(*hexutil.Uint64))) } } + +func TestInteropVariantCheck(t *testing.T) { + testSuite := createInteropTestSuite(t) + gasLimit := uint64(30000000) + gasPrice := big.NewInt(10000000) + privateKey, err := testSuite.HdAccountStore.DerivePrivateKeyAt(uint32(0)) + require.NoError(t, err) + fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + + // TODO: fix when we add a wait for ready on the opsim + time.Sleep(3 * time.Second) + + // Create initiating message using L2ToL2CrossDomainMessenger + origin := common.HexToAddress(l2toL2CrossDomainMessengerAddress) + initiatingMsgNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + parsedSchemaRegistryAbi, _ := abi.JSON(strings.NewReader(opbindings.SchemaRegistryABI)) + data, err := parsedSchemaRegistryAbi.Pack("register", "uint256 value", common.HexToAddress("0x0000000000000000000000000000000000000000"), false) + require.NoError(t, err) + parsedSendMessageABI, err := abi.JSON(strings.NewReader(`[{"constant":false,"inputs":[{"name":"_destination","type":"uint256"},{"name":"_target","type":"address"},{"name":"_message","type":"bytes"}],"name":"sendMessage","outputs":[],"stateMutability":"payable","type":"function"}]`)) + require.NoError(t, err) + sendMessage, err := parsedSendMessageABI.Pack("sendMessage", testSuite.DestChainID, predeploys.SchemaRegistryAddr, data) + require.NoError(t, err) + initiatingMsgTx := types.NewTransaction(initiatingMsgNonce, origin, big.NewInt(0), gasLimit, gasPrice, sendMessage) + require.NoError(t, err) + signedInitiatingMsgTx, err := types.SignTx(initiatingMsgTx, types.NewEIP155Signer(testSuite.SourceChainID), privateKey) + require.NoError(t, err) + err = testSuite.SourceEthClient.SendTransaction(context.Background(), signedInitiatingMsgTx) + require.NoError(t, err) + initiatingMessageTxReceipt, err := bind.WaitMined(context.Background(), testSuite.SourceEthClient, signedInitiatingMsgTx) + require.NoError(t, err) + require.True(t, initiatingMessageTxReceipt.Status == 1, "initiating message transaction failed") + + // Create executing message using CrossL2Inbox + executeMessageNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + crossL2Inbox := crossl2inbox.NewCrossL2Inbox() + initiatingMessageBlock, err := testSuite.SourceEthClient.BlockByNumber(context.Background(), initiatingMessageTxReceipt.BlockNumber) + require.NoError(t, err) + initiatingMessageLog := initiatingMessageTxReceipt.Logs[0] + identifier := crossl2inbox.Identifier{ + Origin: origin, + BlockNumber: initiatingMessageTxReceipt.BlockNumber, + LogIndex: big.NewInt(0), + Timestamp: new(big.Int).SetUint64(initiatingMessageBlock.Time()), + ChainId: new(big.Int).SetUint64(config.DefaultNetworkConfig.L2Configs[0].ChainID), + } + executeMessageCallData, err := crossL2Inbox.Abi.Pack("executeMessage", identifier, fromAddress, initiatingMessageLog.Data) + require.NoError(t, err) + executeMessageTx := types.NewTransaction(executeMessageNonce, predeploys.CrossL2InboxAddr, big.NewInt(0), gasLimit, gasPrice, executeMessageCallData) + require.NoError(t, err) + executeMessageSignedTx, err := types.SignTx(executeMessageTx, types.NewEIP155Signer(testSuite.DestChainID), privateKey) + require.NoError(t, err) + err = testSuite.DestEthClient.SendTransaction(context.Background(), executeMessageSignedTx) + require.NoError(t, err) + executeMessageTxReceipt, err := bind.WaitMined(context.Background(), testSuite.DestEthClient, executeMessageSignedTx) + require.NoError(t, err) + require.True(t, executeMessageTxReceipt.Status == 1, "execute message transaction failed") +} + +func TestInteropInvariantCheckFailsBadLogIndex(t *testing.T) { + testSuite := createInteropTestSuite(t) + gasLimit := uint64(30000000) + gasPrice := big.NewInt(10000000) + privateKey, err := testSuite.HdAccountStore.DerivePrivateKeyAt(uint32(0)) + require.NoError(t, err) + fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + + // TODO: fix when we add a wait for ready on the opsim + time.Sleep(3 * time.Second) + + // Create initiating message using L2ToL2CrossDomainMessenger + origin := common.HexToAddress(l2toL2CrossDomainMessengerAddress) + initiatingMsgNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + parsedSchemaRegistryAbi, _ := abi.JSON(strings.NewReader(opbindings.SchemaRegistryABI)) + data, err := parsedSchemaRegistryAbi.Pack("register", "uint256 value", common.HexToAddress("0x0000000000000000000000000000000000000000"), false) + require.NoError(t, err) + parsedSendMessageABI, err := abi.JSON(strings.NewReader(`[{"constant":false,"inputs":[{"name":"_destination","type":"uint256"},{"name":"_target","type":"address"},{"name":"_message","type":"bytes"}],"name":"sendMessage","outputs":[],"stateMutability":"payable","type":"function"}]`)) + require.NoError(t, err) + sendMessage, err := parsedSendMessageABI.Pack("sendMessage", testSuite.DestChainID, predeploys.SchemaRegistryAddr, data) + require.NoError(t, err) + initiatingMsgTx := types.NewTransaction(initiatingMsgNonce, origin, big.NewInt(0), gasLimit, gasPrice, sendMessage) + require.NoError(t, err) + signedInitiatingMsgTx, err := types.SignTx(initiatingMsgTx, types.NewEIP155Signer(testSuite.SourceChainID), privateKey) + require.NoError(t, err) + err = testSuite.SourceEthClient.SendTransaction(context.Background(), signedInitiatingMsgTx) + require.NoError(t, err) + initiatingMessageTxReceipt, err := bind.WaitMined(context.Background(), testSuite.SourceEthClient, signedInitiatingMsgTx) + require.NoError(t, err) + require.True(t, initiatingMessageTxReceipt.Status == 1, "initiating message transaction failed") + + // Create executing message using CrossL2Inbox + executeMessageNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + crossL2Inbox := crossl2inbox.NewCrossL2Inbox() + initiatingMessageBlock, err := testSuite.SourceEthClient.BlockByNumber(context.Background(), initiatingMessageTxReceipt.BlockNumber) + require.NoError(t, err) + initiatingMessageLog := initiatingMessageTxReceipt.Logs[0] + identifier := crossl2inbox.Identifier{ + Origin: origin, + BlockNumber: initiatingMessageTxReceipt.BlockNumber, + LogIndex: big.NewInt(1), + Timestamp: new(big.Int).SetUint64(initiatingMessageBlock.Time()), + ChainId: new(big.Int).SetUint64(config.DefaultNetworkConfig.L2Configs[0].ChainID), + } + executeMessageCallData, err := crossL2Inbox.Abi.Pack("executeMessage", identifier, fromAddress, initiatingMessageLog.Data) + require.NoError(t, err) + executeMessageTx := types.NewTransaction(executeMessageNonce, predeploys.CrossL2InboxAddr, big.NewInt(0), gasLimit, gasPrice, executeMessageCallData) + require.NoError(t, err) + executeMessageSignedTx, err := types.SignTx(executeMessageTx, types.NewEIP155Signer(testSuite.DestChainID), privateKey) + require.NoError(t, err) + err = testSuite.DestEthClient.SendTransaction(context.Background(), executeMessageSignedTx) + require.Error(t, err) +} + +func TestInteropVariantCheckBadBlockNumber(t *testing.T) { + testSuite := createInteropTestSuite(t) + gasLimit := uint64(30000000) + gasPrice := big.NewInt(10000000) + privateKey, err := testSuite.HdAccountStore.DerivePrivateKeyAt(uint32(0)) + require.NoError(t, err) + fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + + // TODO: fix when we add a wait for ready on the opsim + time.Sleep(3 * time.Second) + + // Create initiating message using L2ToL2CrossDomainMessenger + origin := common.HexToAddress(l2toL2CrossDomainMessengerAddress) + initiatingMsgNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + parsedSchemaRegistryAbi, _ := abi.JSON(strings.NewReader(opbindings.SchemaRegistryABI)) + data, err := parsedSchemaRegistryAbi.Pack("register", "uint256 value", common.HexToAddress("0x0000000000000000000000000000000000000000"), false) + require.NoError(t, err) + parsedSendMessageABI, err := abi.JSON(strings.NewReader(`[{"constant":false,"inputs":[{"name":"_destination","type":"uint256"},{"name":"_target","type":"address"},{"name":"_message","type":"bytes"}],"name":"sendMessage","outputs":[],"stateMutability":"payable","type":"function"}]`)) + require.NoError(t, err) + sendMessage, err := parsedSendMessageABI.Pack("sendMessage", testSuite.DestChainID, predeploys.SchemaRegistryAddr, data) + require.NoError(t, err) + initiatingMsgTx := types.NewTransaction(initiatingMsgNonce, origin, big.NewInt(0), gasLimit, gasPrice, sendMessage) + require.NoError(t, err) + signedInitiatingMsgTx, err := types.SignTx(initiatingMsgTx, types.NewEIP155Signer(testSuite.SourceChainID), privateKey) + require.NoError(t, err) + err = testSuite.SourceEthClient.SendTransaction(context.Background(), signedInitiatingMsgTx) + require.NoError(t, err) + initiatingMessageTxReceipt, err := bind.WaitMined(context.Background(), testSuite.SourceEthClient, signedInitiatingMsgTx) + require.NoError(t, err) + require.True(t, initiatingMessageTxReceipt.Status == 1, "initiating message transaction failed") + + // Create executing message using CrossL2Inbox + executeMessageNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + crossL2Inbox := crossl2inbox.NewCrossL2Inbox() + wrongBlockNumber := new(big.Int).Sub(initiatingMessageTxReceipt.BlockNumber, big.NewInt(1)) + wrongMessageBlock, err := testSuite.SourceEthClient.BlockByNumber(context.Background(), wrongBlockNumber) + require.NoError(t, err) + initiatingMessageLog := initiatingMessageTxReceipt.Logs[0] + identifier := crossl2inbox.Identifier{ + Origin: origin, + BlockNumber: wrongBlockNumber, + LogIndex: big.NewInt(0), + Timestamp: new(big.Int).SetUint64(wrongMessageBlock.Time()), + ChainId: new(big.Int).SetUint64(config.DefaultNetworkConfig.L2Configs[0].ChainID), + } + executeMessageCallData, err := crossL2Inbox.Abi.Pack("executeMessage", identifier, fromAddress, initiatingMessageLog.Data) + require.NoError(t, err) + executeMessageTx := types.NewTransaction(executeMessageNonce, predeploys.CrossL2InboxAddr, big.NewInt(0), gasLimit, gasPrice, executeMessageCallData) + require.NoError(t, err) + executeMessageSignedTx, err := types.SignTx(executeMessageTx, types.NewEIP155Signer(testSuite.DestChainID), privateKey) + require.NoError(t, err) + err = testSuite.DestEthClient.SendTransaction(context.Background(), executeMessageSignedTx) + require.Error(t, err) +} + +func TestInteropVariantCheckBadBlockTimestamp(t *testing.T) { + testSuite := createInteropTestSuite(t) + gasLimit := uint64(30000000) + gasPrice := big.NewInt(10000000) + privateKey, err := testSuite.HdAccountStore.DerivePrivateKeyAt(uint32(0)) + require.NoError(t, err) + fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + + // TODO: fix when we add a wait for ready on the opsim + time.Sleep(3 * time.Second) + + // Create initiating message using L2ToL2CrossDomainMessenger + origin := common.HexToAddress(l2toL2CrossDomainMessengerAddress) + initiatingMsgNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + parsedSchemaRegistryAbi, _ := abi.JSON(strings.NewReader(opbindings.SchemaRegistryABI)) + data, err := parsedSchemaRegistryAbi.Pack("register", "uint256 value", common.HexToAddress("0x0000000000000000000000000000000000000000"), false) + require.NoError(t, err) + parsedSendMessageABI, err := abi.JSON(strings.NewReader(`[{"constant":false,"inputs":[{"name":"_destination","type":"uint256"},{"name":"_target","type":"address"},{"name":"_message","type":"bytes"}],"name":"sendMessage","outputs":[],"stateMutability":"payable","type":"function"}]`)) + require.NoError(t, err) + sendMessage, err := parsedSendMessageABI.Pack("sendMessage", testSuite.DestChainID, predeploys.SchemaRegistryAddr, data) + require.NoError(t, err) + initiatingMsgTx := types.NewTransaction(initiatingMsgNonce, origin, big.NewInt(0), gasLimit, gasPrice, sendMessage) + require.NoError(t, err) + signedInitiatingMsgTx, err := types.SignTx(initiatingMsgTx, types.NewEIP155Signer(testSuite.SourceChainID), privateKey) + require.NoError(t, err) + err = testSuite.SourceEthClient.SendTransaction(context.Background(), signedInitiatingMsgTx) + require.NoError(t, err) + initiatingMessageTxReceipt, err := bind.WaitMined(context.Background(), testSuite.SourceEthClient, signedInitiatingMsgTx) + require.NoError(t, err) + require.True(t, initiatingMessageTxReceipt.Status == 1, "initiating message transaction failed") + + // Create executing message using CrossL2Inbox + executeMessageNonce, err := testSuite.DestEthClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + crossL2Inbox := crossl2inbox.NewCrossL2Inbox() + initiatingMessageBlock, err := testSuite.SourceEthClient.BlockByNumber(context.Background(), initiatingMessageTxReceipt.BlockNumber) + require.NoError(t, err) + initiatingMessageLog := initiatingMessageTxReceipt.Logs[0] + identifier := crossl2inbox.Identifier{ + Origin: origin, + BlockNumber: initiatingMessageTxReceipt.BlockNumber, + LogIndex: big.NewInt(0), + Timestamp: new(big.Int).Sub(new(big.Int).SetUint64(initiatingMessageBlock.Time()), big.NewInt(1)), + ChainId: new(big.Int).SetUint64(config.DefaultNetworkConfig.L2Configs[0].ChainID), + } + executeMessageCallData, err := crossL2Inbox.Abi.Pack("executeMessage", identifier, fromAddress, initiatingMessageLog.Data) + require.NoError(t, err) + executeMessageTx := types.NewTransaction(executeMessageNonce, predeploys.CrossL2InboxAddr, big.NewInt(0), gasLimit, gasPrice, executeMessageCallData) + require.NoError(t, err) + executeMessageSignedTx, err := types.SignTx(executeMessageTx, types.NewEIP155Signer(testSuite.DestChainID), privateKey) + require.NoError(t, err) + err = testSuite.DestEthClient.SendTransaction(context.Background(), executeMessageSignedTx) + require.Error(t, err) +} diff --git a/testutils/mockchain.go b/testutils/mockchain.go index 42e221ff..c49c9200 100644 --- a/testutils/mockchain.go +++ b/testutils/mockchain.go @@ -2,6 +2,7 @@ package testutils import ( "context" + "math/big" "github.com/ethereum-optimism/supersim/config" @@ -64,6 +65,14 @@ func (c *MockChain) EthSendTransaction(ctx context.Context, tx *types.Transactio return nil } +func (c *MockChain) EthBlockByNumber(ctx context.Context, blockHeight *big.Int) (*types.Block, error) { + return &types.Block{}, nil +} + func (c *MockChain) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { return &MockSubscription{}, nil } + +func (c *MockChain) DebugTraceCall(ctx context.Context, txArgs config.TransactionArgs) (config.TraceCallRaw, error) { + return config.TraceCallRaw{}, nil +}