diff --git a/devnet-sdk/descriptors/deployment.go b/devnet-sdk/descriptors/deployment.go index 78fc0d7f9d48a..981569112a663 100644 --- a/devnet-sdk/descriptors/deployment.go +++ b/devnet-sdk/descriptors/deployment.go @@ -2,6 +2,7 @@ package descriptors import ( "encoding/json" + "fmt" "net/http" "github.com/ethereum-optimism/optimism/devnet-sdk/types" @@ -10,6 +11,7 @@ import ( type PortInfo struct { Host string `json:"host"` + Path string `json:"path,omitempty"` Scheme string `json:"scheme,omitempty"` Port int `json:"port,omitempty"` PrivatePort int `json:"private_port,omitempty"` @@ -17,6 +19,14 @@ type PortInfo struct { ReverseProxyHeader http.Header `json:"reverse_proxy_header,omitempty"` } +func AppendPath(baseURL, path string) string { + url := baseURL + if path != "" { + url += fmt.Sprintf("/%s", path) + } + return url +} + // EndpointMap is a map of service names to their endpoints. type EndpointMap map[string]*PortInfo diff --git a/op-acceptance-tests/tests/interop/message/init_test.go b/op-acceptance-tests/tests/interop/message/init_test.go new file mode 100644 index 0000000000000..a1b67e9e6dbb0 --- /dev/null +++ b/op-acceptance-tests/tests/interop/message/init_test.go @@ -0,0 +1,14 @@ +package msg + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +var SimpleInterop presets.TestSetup[*presets.SimpleInterop] + +func TestMain(m *testing.M) { + SimpleInterop = presets.NewSimpleInterop + presets.DoMain(m, presets.ConfigureSimpleInterop()) +} diff --git a/op-acceptance-tests/tests/interop/message/interop_msg_test.go b/op-acceptance-tests/tests/interop/message/interop_msg_test.go new file mode 100644 index 0000000000000..c0a67b2e60e78 --- /dev/null +++ b/op-acceptance-tests/tests/interop/message/interop_msg_test.go @@ -0,0 +1,27 @@ +package msg + +import ( + "math/rand" + "testing" + + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +// TestInitExecMsg tests basic interop messaging +func TestInitExecMsg(gt *testing.T) { + t := devtest.SerialT(gt) + sys := SimpleInterop(t) + rng := rand.New(rand.NewSource(1234)) + alice := sys.FunderA.NewFundedEOA(eth.ThousandEther) + bob := sys.FunderB.NewFundedEOA(eth.ThousandEther) + + eventLoggerAddress := alice.DeployEventLogger() + // Trigger random init message at chain A + initIntent, _ := alice.SendInitMessage(interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(5), rng.Intn(30))) + // Make sure supervisor indexs block which includes init message + sys.Supervisor.AdvanceUnsafeHead(alice.ChainID(), 2) + // Single event in tx so index is 0 + bob.SendExecMessage(initIntent, 0) +} diff --git a/op-devstack/dsl/eoa.go b/op-devstack/dsl/eoa.go index ca4b33620b73b..3c0944c5efa29 100644 --- a/op-devstack/dsl/eoa.go +++ b/op-devstack/dsl/eoa.go @@ -5,10 +5,14 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-service/txintent" "github.com/ethereum-optimism/optimism/op-service/txplan" ) @@ -115,3 +119,33 @@ func (u *EOA) VerifyBalanceExact(v eth.ETH) { actual := u.balance() u.t.Require().Equal(v, actual, "must have expected balance") } + +func (u *EOA) DeployEventLogger() common.Address { + tx := txplan.NewPlannedTx(u.Plan(), txplan.WithData(common.FromHex(bindings.EventloggerBin))) + res, err := tx.Included.Eval(u.ctx) + u.t.Require().NoError(err, "failed to deploy EventLogger") + eventLoggerAddress := res.ContractAddress + u.log.Info("deployed EventLogger", "chainID", tx.ChainID.Value(), "address", eventLoggerAddress) + return eventLoggerAddress +} + +func (u *EOA) SendInitMessage(trigger *txintent.InitTrigger) (*txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput], *types.Receipt) { + tx := txintent.NewIntent[*txintent.InitTrigger, *txintent.InteropOutput](u.Plan()) + tx.Content.Set(trigger) + receipt, err := tx.PlannedTx.Included.Eval(u.ctx) + u.t.Require().NoError(err, "init msg receipt not found") + u.log.Info("init message included", "chain", u.ChainID(), "block", receipt.BlockNumber) + return tx, receipt +} + +func (u *EOA) SendExecMessage(initIntent *txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput], eventIdx int) (*txintent.IntentTx[*txintent.ExecTrigger, *txintent.InteropOutput], *types.Receipt) { + tx := txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](u.Plan()) + tx.Content.DependOn(&initIntent.Result) + tx.Content.Fn(txintent.ExecuteIndexed(constants.CrossL2Inbox, &initIntent.Result, eventIdx)) + receipt, err := tx.PlannedTx.Included.Eval(u.ctx) + u.t.Require().NoError(err, "exec msg receipt not found") + u.log.Info("exec message included", "chain", u.ChainID(), "block", receipt.BlockNumber) + // Check single ExecutingMessage triggered + u.t.Require().Equal(1, len(receipt.Logs)) + return tx, receipt +} diff --git a/op-devstack/dsl/supervisor.go b/op-devstack/dsl/supervisor.go index f0ae67dee1f6e..710bdcd2fce34 100644 --- a/op-devstack/dsl/supervisor.go +++ b/op-devstack/dsl/supervisor.go @@ -3,6 +3,7 @@ package dsl import ( "context" "errors" + "fmt" "time" "github.com/ethereum-optimism/optimism/op-devstack/stack" @@ -100,3 +101,23 @@ func (s *Supervisor) SafeBlockID(chainID eth.ChainID) eth.BlockID { return syncStatus.Chains[chainID].CrossSafe } + +func (s *Supervisor) AdvanceUnsafeHead(chainID eth.ChainID, block uint64) { + initial := s.FetchSyncStatus() + chInitial, ok := initial.Chains[chainID] + s.require.True(ok, fmt.Sprintf("chain sync status not found: chain id: %d", chainID)) + required := chInitial.LocalUnsafe.Number + block + attempts := int(block + 3) // intentionally allow few more attempts for avoid flaking + err := retry.Do0(s.ctx, attempts, &retry.FixedStrategy{Dur: 2 * time.Second}, + func() error { + chStatus := s.FetchSyncStatus().Chains[chainID] + s.log.Info("Supervisor view of unsafe head", "chain", chainID, "unsafe", chStatus.LocalUnsafe) + if chStatus.LocalUnsafe.Number < required { + s.log.Info("Unsafe head sync status not ready", + "chain", chainID, "initialUnsafe", chInitial.LocalUnsafe, "currentUnsafe", chStatus.LocalUnsafe, "minRequired", required) + return fmt.Errorf("expected head to advance") + } + return nil + }) + s.require.NoError(err) +} diff --git a/op-devstack/sysext/helpers.go b/op-devstack/sysext/helpers.go index 56e150c6c4f5e..a152cb4a38f96 100644 --- a/op-devstack/sysext/helpers.go +++ b/op-devstack/sysext/helpers.go @@ -75,14 +75,15 @@ func (orch *Orchestrator) findProtocolService(service *descriptors.Service, prot for proto, endpoint := range service.Endpoints { if proto == protocol { if orch.env.Env.ReverseProxyURL != "" && !orch.useDirectCnx { - return orch.env.Env.ReverseProxyURL, endpoint.ReverseProxyHeader, nil + return descriptors.AppendPath(orch.env.Env.ReverseProxyURL, endpoint.Path), endpoint.ReverseProxyHeader, nil } port := endpoint.Port if orch.usePrivatePorts { port = endpoint.PrivatePort } - return fmt.Sprintf("http://%s:%d", endpoint.Host, port), nil, nil + url := descriptors.AppendPath(fmt.Sprintf("http://%s:%d", endpoint.Host, port), endpoint.Path) + return url, nil, nil } } return "", nil, fmt.Errorf("protocol %s not found", protocol) diff --git a/op-devstack/sysext/l1.go b/op-devstack/sysext/l1.go index d6819bd7a885b..e6ed47bdf1f4a 100644 --- a/op-devstack/sysext/l1.go +++ b/op-devstack/sysext/l1.go @@ -1,6 +1,8 @@ package sysext import ( + "fmt" + "github.com/ethereum-optimism/optimism/op-devstack/shim" "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-service/eth" @@ -52,6 +54,9 @@ func (o *Orchestrator) hydrateL1(system stack.ExtensibleSystem) { } if faucet, ok := env.Env.L1.Services["faucet"]; ok { + for _, endpoint := range faucet.Endpoints { + endpoint.Path = fmt.Sprintf("chain/%s", l1.ChainID().String()) + } l1.AddFaucet(shim.NewFaucet(shim.FaucetConfig{ CommonConfig: commonConfig, Client: o.rpcClient(t, faucet, RPCProtocol), diff --git a/op-devstack/sysext/l2.go b/op-devstack/sysext/l2.go index aa00cb56c7fc1..3609ab2ce6a81 100644 --- a/op-devstack/sysext/l2.go +++ b/op-devstack/sysext/l2.go @@ -1,6 +1,7 @@ package sysext import ( + "fmt" "strings" "github.com/ethereum-optimism/optimism/devnet-sdk/descriptors" @@ -57,6 +58,9 @@ func (o *Orchestrator) hydrateL2(net *descriptors.L2Chain, system stack.Extensib o.hydrateL2ProxydMaybe(net, l2) if faucet, ok := net.Services["faucet"]; ok { + for _, endpoint := range faucet.Endpoints { + endpoint.Path = fmt.Sprintf("chain/%s", l2.ChainID().String()) + } l2.AddFaucet(shim.NewFaucet(shim.FaucetConfig{ CommonConfig: commonConfig, Client: o.rpcClient(t, faucet, RPCProtocol),